theme changes

This commit is contained in:
sgraves committed 2026-10-08 13:27:03 -05:00
1 parent 539ad84cf3
commit a8715c8ec4
21 files changed
+462 -223

No files matched your search

+2
View File
@@ -11,6 +11,8 @@
### Changes from v2.0.7-release
* Fixed Windows setup icon location
* Upgraded `cpp-build-system` to v2.0.1
* Updated theme colors and layout
---
+147 -39
View File
@@ -1,49 +1,157 @@
// constants.dart
import 'package:flutter/material.dart' show GlobalKey, NavigatorState, Color;
import 'package:flutter/material.dart';
import 'package:sodium_libs/sodium_libs.dart';
const accentBlue = Color(0xFF1050A0);
const addMountTitle = 'Add New Mount';
const appLogonTitle = 'Repertory Portal Login';
const appSettingsTitle = 'Portal Settings';
const appTitle = 'Repertory Management Portal';
const borderRadius = 16.0;
const borderRadiusSmall = borderRadius / 2.0;
const borderRadiusTiny = borderRadiusSmall / 2.0;
const boxShadowAlpha = 0.20;
const databaseTypeList = ['rocksdb', 'sqlite'];
const dialogAlpha = 0.95;
const downloadTypeList = ['default', 'direct', 'ring_buffer'];
const dropDownAlpha = 99.0;
const eventLevelList = ['critical', 'error', 'warn', 'info', 'debug', 'trace'];
const gradientColors = [Color(0xFF0A0F1F), Color(0xFF1B1C1F)];
const gradientColors2 = [Color(0x07FFFFFF), Color(0x00000000)];
const highlightAlpha = 0.10;
const largeIconSize = 32.0;
const loginIconSize = 54.0;
const logonWidth = 420.0;
const outlineAlpha = 0.15;
const padding = 16.0;
const paddingLarge = padding * 2.0;
const paddingMedium = 12.0;
const paddingSmall = padding / 2.0;
const primaryAlpha = 0.12;
const primarySurfaceAlpha = 92.0;
const protocolTypeList = ['http', 'https'];
const providerTypeList = ['Encrypt', 'Remote', 'S3', 'Sia'];
const ringBufferSizeList = ['128', '256', '512', '1024', '2048'];
const secondaryAlpha = 0.45;
const secondarySurfaceAlpha = 70.0;
const smallIconSize = 18.0;
const surfaceContainerLowDark = Color(0xFF292A2D);
const surfaceDark = Color(0xFF202124);
// ---------------------------------------------------------------------------
// App strings
// Window/screen titles shown in the UI.
// ---------------------------------------------------------------------------
const addMountTitle = 'Add New Mount'; // Title for the add-mount dialog.
const appLogonTitle = 'Repertory Portal Login'; // Title for the login screen.
const appSettingsTitle = 'Portal Settings'; // Title for the settings dialog.
const appTitle = 'Repertory Management Portal'; // App/home screen title.
// ---------------------------------------------------------------------------
// Palette
// Seed/accent blue, muted dark-theme primary, dark grey surfaces, error red.
// ---------------------------------------------------------------------------
const accentBlue = Color(0xFF1050A0); // Seed color the ColorScheme is built from.
const primaryBlue = Color(0xFF6E95C9); // Muted primary accent used in dark mode.
const surfaceDark = Color(0xFF1E2127); // Scaffold/base surface (blue-tinted grey).
const surfaceContainerLowDark = Color(0xFF272B33); // Elevated surface (cards/dialogs).
const errorRed = Color(0xFFD98A86); // Muted error red for validation/error text.
// ---------------------------------------------------------------------------
// Gradients
// Full-screen background and the subtle FAB sheen overlay.
// ---------------------------------------------------------------------------
const gradientColors = [Color(0xFF0A0F1F), Color(0xFF1B1C1F)]; // App background gradient.
const gradientColors2 = [Color(0x07FFFFFF), Color(0x00000000)]; // Faint top-down FAB sheen.
// ---------------------------------------------------------------------------
// Alpha tints
// Translucency applied over primary/surface/outline colors.
// ---------------------------------------------------------------------------
const boxShadowAlpha = 0.20; // Opacity of drop shadows.
const dialogTintAlpha = 0.06; // Accent tint blended onto dialog/dropdown surfaces.
const highlightAlpha = 0.10; // Subtle highlight/border tint.
const outlineAlpha = 0.15; // Outline/border and scrim tint.
const primaryAlpha = 0.12; // Primary fill tint for cards/fields.
const primarySurfaceAlpha = 92.0; // Near-opaque primary surface text/icon tint.
const secondaryAlpha = 0.45; // Medium primary tint for buttons/toggles.
const secondarySurfaceAlpha = 70.0; // Dimmer surface text tint (descriptions).
// ---------------------------------------------------------------------------
// Corner radii
// Shared rounding scale for cards, buttons, and dialogs.
// ---------------------------------------------------------------------------
const borderRadius = 16.0; // Base corner radius.
const borderRadiusSmall = borderRadius / 2.0; // Half radius for cards/fields (8).
const borderRadiusTiny = borderRadiusSmall / 2.0; // Quarter radius for small chips (4).
// ---------------------------------------------------------------------------
// Border widths
// Default hairline and the thicker focused-input border.
// ---------------------------------------------------------------------------
const borderWidth = 1.0; // Standard hairline border for bevels.
const focusBorderWidth = 2.0; // Emphasized border for focused inputs.
// ---------------------------------------------------------------------------
// Elevations
// Card drop shadow and the stronger floating-element shadow.
// ---------------------------------------------------------------------------
const cardElevation = 4.0; // Subtle shadow for content cards.
const fabElevation = 12.0; // Stronger shadow for the floating action button.
// ---------------------------------------------------------------------------
// Spacing
// Shared padding scale used across layouts.
// ---------------------------------------------------------------------------
const padding = 16.0; // Base padding/gap unit.
const paddingLarge = padding * 2.0; // Double padding for larger gaps (32).
const paddingMedium = 12.0; // Medium padding between base and small.
const paddingSmall = padding / 2.0; // Half padding for tight gaps (8).
// ---------------------------------------------------------------------------
// Icon sizes
// Shared scale for iconography.
// ---------------------------------------------------------------------------
const largeIconSize = 32.0; // Large icons (FAB, prominent actions).
const loginIconSize = 54.0; // Oversized icon on the login screen.
const smallIconSize = 18.0; // Small inline icons.
// ---------------------------------------------------------------------------
// Icons
// Shared iconography reused across the UI (actions, toggles, fields).
// ---------------------------------------------------------------------------
const iconAdd = Icons.add; // Add / create actions (FAB, add mount).
const iconAutoMount = Icons.auto_mode; // Auto-mount indicator.
const iconBack = Icons.arrow_back; // Back navigation.
const iconClose = Icons.close; // Close/dismiss (dialogs).
const iconDelete = Icons.delete; // Delete/remove actions.
const iconEdit = Icons.edit; // Edit actions.
const iconFolder = Icons.folder; // Folder/path and app logo fallback.
const iconLock = Icons.lock; // Password/lock fields.
const iconPassword = Icons.password; // Password-style settings.
const iconRename = Icons.drive_file_rename_outline; // Rename/config-name field.
const iconRemoteMount = Icons.lan; // Remote mount (networking).
const iconSettings = Icons.settings; // Settings/gear actions.
const iconTest = Icons.check; // Test/confirm actions.
const iconToggleOff = Icons.toggle_off; // Toggle (off state).
const iconToggleOn = Icons.toggle_on; // Toggle (on state).
const iconUser = Icons.person; // User/username fields.
const iconVisibility = Icons.visibility; // Reveal password.
const iconVisibilityOff = Icons.visibility_off; // Hide password.
// ---------------------------------------------------------------------------
// Control sizes
// Fixed dimensions for buttons, framed controls, and the FAB.
// ---------------------------------------------------------------------------
const buttonHeight = 40.0; // Minimum height of primary action buttons.
const fabInnerSize = 56.0; // Inner tappable square of the add-mount FAB.
const framedControlSize = 46.0; // Bordered icon buttons on mount cards.
const headerControlSize = 40.0; // App-bar logo/back box.
const mountCardMinHeight = 120.0; // Minimum height of a mount list card.
// ---------------------------------------------------------------------------
// Layout widths/factors
// Fixed widths and viewport-fraction sizing.
// ---------------------------------------------------------------------------
const logonWidth = 420.0; // Fixed width of the login card.
const snackBarWidthFactor = 0.50; // Snackbar width as a fraction of the viewport.
// ---------------------------------------------------------------------------
// Dialog sizing
// Viewport-fraction width/height with absolute pixel clamps.
// ---------------------------------------------------------------------------
const dialogMinWidthFactor = 0.40; // Min dialog width as a viewport fraction.
const dialogMaxWidthFactor = 0.70; // Max dialog width as a viewport fraction.
const dialogMaxHeightFactor = 0.85; // Max dialog height before it scrolls.
const dialogMinWidth = 360.0; // Absolute minimum dialog width (px).
const dialogMaxWidth = 820.0; // Absolute maximum dialog width (px).
// ---------------------------------------------------------------------------
// Option lists
// Selectable values for configuration dropdowns.
// ---------------------------------------------------------------------------
const databaseTypeList = ['rocksdb', 'sqlite']; // Database backend options.
const downloadTypeList = ['default', 'direct', 'ring_buffer']; // Download strategy options.
const eventLevelList = ['critical', 'error', 'warn', 'info', 'debug', 'trace']; // Log level options.
const protocolTypeList = ['http', 'https']; // Connection protocol options.
const providerTypeList = ['Encrypt', 'Remote', 'S3', 'Sia']; // Mount provider options.
const ringBufferSizeList = ['128', '256', '512', '1024', '2048']; // Ring-buffer size options (MiB).
// ---------------------------------------------------------------------------
// Runtime globals
// Navigator key and lazily-initialized libsodium instance.
// ---------------------------------------------------------------------------
// Enables navigation/context access from outside the widget tree.
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Sodium? _sodium;
Sodium? _sodium; // Backing store for the libsodium instance.
void setSodium(Sodium sodium) {
_sodium = sodium;
_sodium = sodium; // Set once during app initialization.
}
Sodium get sodium => _sodium!;
Sodium get sodium => _sodium!; // Access the initialized libsodium instance.
+38 -8
View File
@@ -18,10 +18,14 @@ Future doShowDialog(BuildContext context, Widget child) => showDialog(
return Theme(
data: theme.copyWith(
dialogTheme: DialogThemeData(
backgroundColor: darken(
scheme.primary,
0.95,
).withValues(alpha: constants.dropDownAlpha),
// Elevated surface: slightly lighter than the scaffold background
// (surfaceDark) with a faint accent tint so the popup reads as part
// of the dark theme rather than a near-black panel. The backdrop blur
// and barrier scrim already provide separation, so keep it opaque.
backgroundColor: Color.alphaBlend(
scheme.primary.withValues(alpha: constants.dialogTintAlpha),
constants.surfaceContainerLowDark,
),
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(constants.borderRadius),
@@ -29,7 +33,7 @@ Future doShowDialog(BuildContext context, Widget child) => showDialog(
color: scheme.outlineVariant.withValues(
alpha: constants.outlineAlpha,
),
width: 1,
width: constants.borderWidth,
),
),
),
@@ -90,8 +94,8 @@ final Validator portIsValid = (value) {
// ignore: prefer_function_declarations_over_variables
final Validator trimNotEmptyValidator = (value) => value.trim().isNotEmpty;
createUriValidator<Validator>({host, port}) {
return (value) =>
Validator createUriValidator({Object? host, Object? port}) {
return (String value) =>
Uri.tryParse('http://${host ?? value}:${port ?? value}/') != null;
}
@@ -448,14 +452,39 @@ InputDecoration createCommonDecoration(
filled: filled,
fillColor: colorScheme.primary.withValues(alpha: constants.primaryAlpha),
hintText: hintText,
// Keep label and prefix/suffix icons neutral so the error state does not
// flood the whole field with red; the error signal stays on the border and
// helper text only.
labelStyle: TextStyle(color: colorScheme.onSurface),
floatingLabelStyle: TextStyle(color: colorScheme.onSurface),
prefixIconColor: colorScheme.onSurface,
suffixIconColor: colorScheme.onSurface,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
borderSide: BorderSide(
color: colorScheme.primary,
width: constants.focusBorderWidth,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
borderSide: BorderSide(
color: colorScheme.error,
width: constants.borderWidth,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
borderSide: BorderSide(
color: colorScheme.error,
width: constants.focusBorderWidth,
),
),
errorStyle: TextStyle(color: colorScheme.error),
contentPadding: const EdgeInsets.all(constants.paddingSmall),
);
@@ -504,3 +533,4 @@ Color darken(Color color, [double percentage = 0.1]) {
);
return hslDark.toColor();
}
+33 -41
View File
@@ -3,15 +3,10 @@
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/models/settings.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:repertory/widgets/auth_check.dart';
import 'package:sodium_libs/sodium_libs.dart' show SodiumInit;
@@ -47,36 +42,57 @@ class _MyAppState extends State<MyApp> {
@override
Widget build(context) {
final snackBarTheme = SnackBarThemeData(
width: MediaQuery.of(context).size.width * 0.50,
width:
MediaQuery.of(context).size.width * constants.snackBarWidthFactor,
behavior: SnackBarBehavior.floating,
);
final colorScheme = ColorScheme.fromSeed(
brightness: Brightness.dark,
seedColor: constants.accentBlue,
primary: constants.primaryBlue,
error: constants.errorRed,
onSurface: Colors.white70,
surface: constants.surfaceDark,
surfaceContainerLow: constants.surfaceContainerLowDark,
);
return MaterialApp(
navigatorKey: constants.navigatorKey,
themeMode: ThemeMode.dark,
darkTheme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
brightness: Brightness.dark,
seedColor: constants.accentBlue,
onSurface: Colors.white70,
surface: constants.surfaceDark,
surfaceContainerLow: constants.surfaceContainerLowDark,
),
colorScheme: colorScheme,
scaffoldBackgroundColor: constants.surfaceDark,
snackBarTheme: snackBarTheme,
appBarTheme: const AppBarTheme(scrolledUnderElevation: 0),
inputDecorationTheme: const InputDecorationTheme(
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(width: 2)),
cardTheme: CardThemeData(
elevation: constants.cardElevation,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
side: BorderSide(
color: colorScheme.outlineVariant.withValues(
alpha: constants.highlightAlpha,
),
width: constants.borderWidth,
),
),
),
inputDecorationTheme: InputDecorationTheme(
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(width: constants.focusBorderWidth),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(40),
minimumSize: const Size.fromHeight(constants.buttonHeight),
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(minimumSize: const Size.fromHeight(40)),
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(constants.buttonHeight),
),
),
dividerTheme: const DividerThemeData(thickness: 0.6, space: 0),
),
@@ -85,31 +101,7 @@ class _MyAppState extends State<MyApp> {
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} Settings • ${formatMountName(mount.type, mount.name)}',
),
);
},
);
},
);
}
+3 -3
View File
@@ -125,7 +125,7 @@ class Mount with ChangeNotifier {
return;
}
return refresh();
return await refresh();
} catch (e) {
debugPrint('$e');
}
@@ -154,7 +154,7 @@ class Mount with ChangeNotifier {
return;
}
return refresh();
return await refresh();
} catch (e) {
debugPrint('$e');
}
@@ -330,7 +330,7 @@ class Mount with ChangeNotifier {
return;
}
return refresh();
return await refresh();
} catch (e) {
debugPrint('$e');
}
+26 -23
View File
@@ -10,21 +10,31 @@ import 'package:repertory/models/mount.dart';
import 'package:repertory/models/mount_list.dart';
import 'package:repertory/types/mount_config.dart';
import 'package:repertory/utils/safe_set_state_mixin.dart';
import 'package:repertory/widgets/app_dialog.dart';
import 'package:repertory/widgets/app_dropdown.dart';
import 'package:repertory/widgets/app_outlined_icon_button.dart';
import 'package:repertory/widgets/app_scaffold.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();
/// Opens the "Add New Mount" modal dialog.
Future<void> showAddMountDialog(BuildContext context) {
return doShowDialog(
context,
const AppDialog(
title: constants.addMountTitle,
child: AddMountContent(),
),
);
}
class _AddMountScreenState extends State<AddMountScreen>
with SafeSetState<AddMountScreen> {
class AddMountContent extends StatefulWidget {
const AddMountContent({super.key});
@override
State<AddMountContent> createState() => _AddMountContentState();
}
class _AddMountContentState extends State<AddMountContent>
with SafeSetState<AddMountContent> {
Mount? _mount;
final _mountNameController = TextEditingController();
String _mountType = "";
@@ -48,9 +58,9 @@ class _AddMountScreenState extends State<AddMountScreen>
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return AppScaffold(
title: widget.title,
showBack: true,
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AppDropdown<String>(
constrainToIntrinsic: true,
@@ -83,18 +93,13 @@ class _AddMountScreenState extends State<AddMountScreen>
scheme,
'Configuration Name',
hintText: 'Enter a unique name',
icon: Icons.drive_file_rename_outline,
icon: constants.iconRename,
),
),
],
if (_mount != null) ...[
const SizedBox(height: constants.padding),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: constants.padding,
),
child: ClipRRect(
ClipRRect(
borderRadius: BorderRadius.circular(constants.borderRadius),
child: MountSettingsWidget(
isAdd: true,
@@ -103,15 +108,13 @@ class _AddMountScreenState extends State<AddMountScreen>
showAdvanced: false,
),
),
),
),
const SizedBox(height: constants.padding),
Row(
children: [
IntrinsicWidth(
child: AppOutlinedIconButton(
enabled: _enabled,
icon: Icons.check,
icon: constants.iconTest,
text: 'Test',
onPressed: _handleProviderTest,
),
@@ -120,7 +123,7 @@ class _AddMountScreenState extends State<AddMountScreen>
IntrinsicWidth(
child: AppOutlinedIconButton(
enabled: _enabled,
icon: Icons.add,
icon: constants.iconAdd,
text: 'Add',
onPressed: () async {
setState(() {
+5 -5
View File
@@ -198,7 +198,7 @@ class _AuthScreenState extends State<AuthScreen> {
fit: BoxFit.contain,
errorBuilder: (_, _, _) {
return Icon(
Icons.folder,
constants.iconFolder,
color: scheme.primary,
size: constants.loginIconSize,
);
@@ -234,7 +234,7 @@ class _AuthScreenState extends State<AuthScreen> {
AppTextField(
autofocus: true,
controller: _userController,
icon: Icons.person,
icon: constants.iconUser,
labelText: 'Username',
textInputAction: TextInputAction.next,
validator: (v) {
@@ -250,7 +250,7 @@ class _AuthScreenState extends State<AuthScreen> {
const SizedBox(height: constants.padding),
AppTextField(
controller: _passwordController,
icon: Icons.lock,
icon: constants.iconLock,
labelText: 'Password',
obscureText: _obscure,
suffixIcon: IconButton(
@@ -259,8 +259,8 @@ class _AuthScreenState extends State<AuthScreen> {
: 'Hide password',
icon: Icon(
_obscure
? Icons.visibility
: Icons.visibility_off,
? constants.iconVisibility
: constants.iconVisibilityOff,
),
onPressed: () {
setState(() {
@@ -3,32 +3,38 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart';
import 'package:repertory/models/mount.dart';
import 'package:repertory/utils/safe_set_state_mixin.dart';
import 'package:repertory/widgets/app_scaffold.dart';
import 'package:repertory/widgets/app_dialog.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();
/// Opens the "Edit Mount" modal dialog for the given [mount].
Future<void> showEditMountDialog(BuildContext context, Mount mount) {
return doShowDialog(context, EditMountContent(mount: mount));
}
class _EditMountScreenState extends State<EditMountScreen>
with SafeSetState<EditMountScreen> {
class EditMountContent extends StatefulWidget {
final Mount mount;
const EditMountContent({super.key, required this.mount});
@override
State<EditMountContent> createState() => _EditMountContentState();
}
class _EditMountContentState extends State<EditMountContent>
with SafeSetState<EditMountContent> {
bool _showAdvanced = false;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return AppScaffold(
title: widget.title,
showBack: true,
advancedWidget: Row(
return AppDialog(
title:
'${widget.mount.provider} Settings • ${formatMountName(widget.mount.type, widget.mount.name)}',
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
@@ -38,22 +44,19 @@ class _EditMountScreenState extends State<EditMountScreen>
const SizedBox(width: 6),
IconButton(
tooltip: _showAdvanced ? 'Hide advanced' : 'Show advanced',
icon: Icon(_showAdvanced ? Icons.toggle_on : Icons.toggle_off),
icon: Icon(
_showAdvanced ? constants.iconToggleOn : constants.iconToggleOff,
),
color: _showAdvanced ? scheme.primary : scheme.onSurface,
onPressed: () => setState(() => _showAdvanced = !_showAdvanced),
),
],
),
children: [
Expanded(
child: MountSettingsWidget(
mount: widget.mount,
settings: jsonDecode(jsonEncode(widget.mount.mountConfig.settings)),
showAdvanced: _showAdvanced,
),
),
const SizedBox(height: constants.padding),
],
);
}
}
@@ -9,27 +9,27 @@ import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart';
import 'package:repertory/models/auth.dart';
import 'package:repertory/utils/safe_set_state_mixin.dart';
import 'package:repertory/widgets/app_scaffold.dart';
import 'package:repertory/widgets/app_dialog.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();
/// Opens the "Portal Settings" modal dialog.
Future<void> showPortalSettingsDialog(BuildContext context) {
return doShowDialog(context, const PortalSettingsContent());
}
class _EditSettingsScreenState extends State<EditSettingsScreen>
with SafeSetState<EditSettingsScreen> {
class PortalSettingsContent extends StatefulWidget {
const PortalSettingsContent({super.key});
@override
State<PortalSettingsContent> createState() => _PortalSettingsContentState();
}
class _PortalSettingsContentState extends State<PortalSettingsContent>
with SafeSetState<PortalSettingsContent> {
@override
Widget build(BuildContext context) {
return AppScaffold(
title: widget.title,
showBack: true,
showUISettings: true,
children: [
Expanded(
return AppDialog(
title: constants.appSettingsTitle,
child: FutureBuilder<Map<String, dynamic>>(
future: _grabSettings(),
initialData: const <String, dynamic>{},
@@ -45,9 +45,6 @@ class _EditSettingsScreenState extends State<EditSettingsScreen>
);
},
),
),
const SizedBox(height: constants.padding),
],
);
}
+7 -6
View File
@@ -2,6 +2,7 @@
import 'package:flutter/material.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/screens/add_mount_screen.dart';
import 'package:repertory/widgets/app_scaffold.dart';
import 'package:repertory/widgets/mount_list_widget.dart';
@@ -26,7 +27,7 @@ class _HomeScreeState extends State<HomeScreen> {
tag: 'add_mount_fab',
child: Material(
color: scheme.primary.withValues(alpha: constants.secondaryAlpha),
elevation: 12,
elevation: constants.fabElevation,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(constants.borderRadius),
),
@@ -37,7 +38,7 @@ class _HomeScreeState extends State<HomeScreen> {
color: scheme.outlineVariant.withValues(
alpha: constants.outlineAlpha,
),
width: 1,
width: constants.borderWidth,
),
gradient: const LinearGradient(
begin: Alignment.topCenter,
@@ -57,13 +58,13 @@ class _HomeScreeState extends State<HomeScreen> {
child: InkWell(
borderRadius: BorderRadius.circular(constants.borderRadius),
onTap: () {
Navigator.pushNamed(context, '/add');
showAddMountDialog(context);
},
child: const SizedBox(
width: 56,
height: 56,
width: constants.fabInnerSize,
height: constants.fabInnerSize,
child: Center(
child: Icon(Icons.add, size: constants.largeIconSize),
child: Icon(constants.iconAdd, size: constants.largeIconSize),
),
),
),
+4 -4
View File
@@ -20,7 +20,7 @@ void createBooleanSetting(
widget,
Function setState, {
String? description,
IconData icon = Icons.quiz,
IconData icon = constants.iconToggleOn,
}) {
if (!isAdvanced || showAdvanced) {
list.add(
@@ -149,7 +149,7 @@ void createPasswordSetting(
widget,
Function setState, {
String? description,
IconData icon = Icons.password,
IconData icon = constants.iconPassword,
List<Validator> validators = const [],
}) {
if (!isAdvanced || widget.showAdvanced) {
@@ -222,8 +222,8 @@ void createPasswordSetting(
),
icon: Icon(
hidePassword1
? Icons.visibility
: Icons.visibility_off,
? constants.iconVisibility
: constants.iconVisibilityOff,
),
),
],
+105
View File
@@ -0,0 +1,105 @@
// app_dialog.dart
import 'package:flutter/material.dart';
import 'package:repertory/constants.dart' as constants;
/// A themed modal dialog shell that sizes to its content.
///
/// The width is sized between [constants.dialogMinWidthFactor] and
/// [constants.dialogMaxWidthFactor] of the viewport (clamped to the absolute
/// pixel bounds in [constants]). The dialog hugs its content vertically and
/// only begins scrolling once the content would exceed
/// [constants.dialogMaxHeightFactor] of the viewport. For this to work, the
/// content ([child]) must size itself to its intrinsic height (e.g. settings
/// lists built with `shrinkWrap: true`) rather than expanding to fill.
///
/// A fixed header row (title, optional [trailing] widget, close button) stays
/// pinned above the scrollable content.
class AppDialog extends StatelessWidget {
const AppDialog({
super.key,
required this.title,
required this.child,
this.trailing,
});
final String title;
final Widget child;
final Widget? trailing;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final media = MediaQuery.of(context).size;
final maxWidth = (media.width * constants.dialogMaxWidthFactor).clamp(
constants.dialogMinWidth,
constants.dialogMaxWidth,
);
final minWidth = (media.width * constants.dialogMinWidthFactor).clamp(
constants.dialogMinWidth,
maxWidth,
);
final maxHeight = media.height * constants.dialogMaxHeightFactor;
final header = Row(
children: [
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
color: scheme.primary,
letterSpacing: 0.2,
),
),
),
if (trailing != null) ...[
const SizedBox(width: constants.padding),
trailing!,
],
const SizedBox(width: constants.paddingSmall),
IconButton(
tooltip: 'Close',
icon: const Icon(constants.iconClose),
onPressed: () => Navigator.of(context).pop(),
),
],
);
return Dialog(
insetPadding: const EdgeInsets.all(constants.padding),
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: minWidth,
maxWidth: maxWidth,
maxHeight: maxHeight,
),
child: Padding(
// Tighter top/bottom than left/right so the dialog does not look
// vertically empty around the settings list.
padding: const EdgeInsets.symmetric(
horizontal: constants.padding,
vertical: constants.paddingSmall,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
header,
const SizedBox(height: constants.paddingSmall),
// Hug the content height; scroll only once it would exceed the
// dialog's max height.
Flexible(
child: SingleChildScrollView(child: child),
),
],
),
),
),
);
}
}
+4 -1
View File
@@ -94,7 +94,10 @@ class AppDropdown<T> extends StatelessWidget {
final effectiveFill =
fillColor ??
darken(scheme.primary, 0.95).withValues(alpha: constants.dropDownAlpha);
Color.alphaBlend(
scheme.primary.withValues(alpha: constants.dialogTintAlpha),
constants.surfaceContainerLowDark,
);
final effectiveTextStyle =
textStyle ??
@@ -26,8 +26,8 @@ class AppIconButtonFramed extends StatelessWidget {
onTap: onPressed,
borderRadius: radius,
child: Ink(
width: 46,
height: 46,
width: constants.framedControlSize,
height: constants.framedControlSize,
decoration: BoxDecoration(
color: scheme.primary.withValues(alpha: constants.outlineAlpha),
borderRadius: radius,
@@ -35,7 +35,7 @@ class AppIconButtonFramed extends StatelessWidget {
color: scheme.outlineVariant.withValues(
alpha: constants.outlineAlpha,
),
width: 1,
width: constants.borderWidth,
),
boxShadow: [
BoxShadow(
+13 -12
View File
@@ -5,6 +5,7 @@ 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/screens/edit_settings_screen.dart';
import 'package:repertory/widgets/aurora_sweep.dart';
class AppScaffold extends StatelessWidget {
@@ -67,14 +68,14 @@ class AppScaffold extends StatelessWidget {
children: [
if (!showBack) ...[
SizedBox(
width: 40,
height: 40,
width: constants.headerControlSize,
height: constants.headerControlSize,
child: Image.asset(
'assets/images/repertory.png',
fit: BoxFit.contain,
errorBuilder: (_, _, _) {
return Icon(
Icons.folder,
constants.iconFolder,
color: scheme.primary,
size: constants.largeIconSize,
);
@@ -92,8 +93,8 @@ class AppScaffold extends StatelessWidget {
),
onTap: () => Navigator.of(context).pop(),
child: Ink(
width: 40,
height: 40,
width: constants.headerControlSize,
height: constants.headerControlSize,
decoration: BoxDecoration(
color: scheme.surface.withValues(
alpha: constants.secondaryAlpha,
@@ -105,7 +106,7 @@ class AppScaffold extends StatelessWidget {
color: scheme.outlineVariant.withValues(
alpha: constants.highlightAlpha,
),
width: 1,
width: constants.borderWidth,
),
boxShadow: [
BoxShadow(
@@ -117,7 +118,7 @@ class AppScaffold extends StatelessWidget {
),
],
),
child: const Icon(Icons.arrow_back),
child: Icon(constants.iconBack),
),
),
),
@@ -141,8 +142,8 @@ class AppScaffold extends StatelessWidget {
builder: (context, settings, _) => IconButton(
icon: Icon(
settings.enableAnimations
? Icons.toggle_on
: Icons.toggle_off,
? constants.iconToggleOn
: constants.iconToggleOff,
),
color: settings.enableAnimations
? scheme.primary
@@ -157,8 +158,8 @@ class AppScaffold extends StatelessWidget {
builder: (context, settings, _) => IconButton(
icon: Icon(
settings.autoStart
? Icons.toggle_on
: Icons.toggle_off,
? constants.iconToggleOn
: constants.iconToggleOff,
),
color: settings.autoStart
? scheme.primary
@@ -175,7 +176,7 @@ class AppScaffold extends StatelessWidget {
tooltip: 'Settings',
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.pushNamed(context, '/settings');
showPortalSettingsDialog(context);
},
),
const SizedBox(width: constants.padding),
@@ -1,6 +1,7 @@
// app_toggle_button_framed.dart
import 'package:flutter/material.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/widgets/app_icon_button_framed.dart';
class AppToggleButtonFramed extends StatelessWidget {
@@ -22,7 +23,7 @@ class AppToggleButtonFramed extends StatelessWidget {
Color iconColor = scheme.onSurface;
if (mounted != null) {
icon = isOn ? Icons.toggle_on : Icons.toggle_off;
icon = isOn ? constants.iconToggleOn : constants.iconToggleOff;
iconColor = isOn ? scheme.primary : scheme.onSurface;
}
@@ -96,7 +96,7 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
widget.settings,
key,
value,
Icons.person,
constants.iconUser,
true,
widget.showAdvanced,
widget,
@@ -389,11 +389,11 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
final titleStyle = theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
color: scheme.onSurface,
color: scheme.primary,
);
return SettingsList(
shrinkWrap: false,
shrinkWrap: true,
platform: DevicePlatform.web,
lightTheme: settingsTheme,
darkTheme: settingsTheme,
@@ -840,6 +840,7 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
widget,
setState,
description: getSettingDescription('$key.$subKey'),
icon: constants.iconRemoteMount,
);
remoteMountSettings.insertAll(0, tempSettings);
}
+12 -22
View File
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart';
import 'package:repertory/models/mount.dart';
import 'package:repertory/screens/edit_mount_screen.dart';
import 'package:repertory/utils/safe_set_state_mixin.dart';
import 'package:repertory/widgets/app_outlined_icon_button.dart';
import 'package:repertory/widgets/app_icon_button_framed.dart';
@@ -40,20 +41,10 @@ class _MountWidgetState extends State<MountWidget>
);
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 120),
constraints: const BoxConstraints(minHeight: constants.mountCardMinHeight),
child: Card(
margin: const EdgeInsets.all(0.0),
elevation: 12,
color: scheme.primary.withValues(alpha: constants.primaryAlpha),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
side: BorderSide(
color: scheme.outlineVariant.withValues(
alpha: constants.outlineAlpha,
),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(constants.padding),
child: Consumer<Mount>(
@@ -66,12 +57,8 @@ class _MountWidgetState extends State<MountWidget>
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AppIconButtonFramed(
icon: Icons.settings,
onPressed: () => Navigator.pushNamed(
context,
'/edit',
arguments: mount,
),
icon: constants.iconSettings,
onPressed: () => showEditMountDialog(context, mount),
),
const SizedBox(width: constants.padding),
Expanded(
@@ -88,13 +75,13 @@ class _MountWidgetState extends State<MountWidget>
),
if (mount.mounted == false) ...[
AppIconButtonFramed(
icon: Icons.delete,
icon: constants.iconDelete,
onPressed: _enabled
? () async {
? () {
setState(() {
_enabled = false;
});
return doShowDialog(
doShowDialog(
context,
AlertDialog(
actions: [
@@ -102,6 +89,9 @@ class _MountWidgetState extends State<MountWidget>
child: const Text('Yes'),
onPressed: () async {
await mount.remove();
if (!context.mounted) {
return;
}
setState(() {
_enabled = true;
});
@@ -140,7 +130,7 @@ class _MountWidgetState extends State<MountWidget>
children: [
AppOutlinedIconButton(
text: 'Edit path',
icon: Icons.edit,
icon: constants.iconEdit,
enabled: _enabled && mount.mounted == false,
onPressed: () async {
setState(() {
@@ -211,7 +201,7 @@ class _MountWidgetState extends State<MountWidget>
mainAxisSize: MainAxisSize.min,
children: const [
Icon(
Icons.auto_mode,
constants.iconAutoMount,
size: constants.smallIconSize,
),
SizedBox(width: constants.paddingSmall),
@@ -1,11 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_settings_ui/src/sections/abstract_settings_section.dart';
import 'package:flutter_settings_ui/src/sections/platforms/android_settings_section.dart';
import 'package:flutter_settings_ui/src/sections/platforms/ios_settings_section.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:repertory/widgets/settings/web_settings_section.dart';
import 'package:flutter_settings_ui/src/tiles/abstract_settings_tile.dart';
import 'package:flutter_settings_ui/src/utils/platform_utils.dart';
import 'package:flutter_settings_ui/src/utils/settings_theme.dart';
// These platform section widgets are not re-exported by the public barrel, so
// they must be imported directly. They are only reached on non-web platforms;
// this app targets web, but the switch below handles them for completeness.
// ignore: implementation_imports
import 'package:flutter_settings_ui/src/sections/platforms/android_settings_section.dart';
// ignore: implementation_imports
import 'package:flutter_settings_ui/src/sections/platforms/ios_settings_section.dart';
class SettingsSection extends AbstractSettingsSection {
const SettingsSection({
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:repertory/constants.dart' as constants;
// Container(
// height: 65 * scaleFactor,
@@ -30,7 +31,6 @@ class WebSettingsSection extends StatelessWidget {
Widget buildSectionBody(BuildContext context) {
final theme = SettingsTheme.of(context);
final scaleFactor = MediaQuery.textScalerOf(context).scale(1);
return Padding(
padding: margin ?? EdgeInsets.zero,
@@ -39,10 +39,10 @@ class WebSettingsSection extends StatelessWidget {
children: [
if (title != null)
Padding(
padding: EdgeInsetsDirectional.only(
bottom: 5 * scaleFactor,
padding: const EdgeInsetsDirectional.only(
bottom: constants.paddingSmall,
start: 6,
top: 20 * scaleFactor,
top: constants.paddingSmall,
),
child: title!,
),
@@ -50,7 +50,7 @@ class WebSettingsSection extends StatelessWidget {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 4,
elevation: constants.cardElevation,
color: theme.themeData.settingsSectionBackground,
child: buildTileList(),
),
+3 -3
View File
@@ -94,7 +94,7 @@ class _UISettingsWidgetState extends State<UISettingsWidget>
widget.settings,
key,
value,
Icons.person,
constants.iconUser,
false,
widget.showAdvanced,
widget,
@@ -109,11 +109,11 @@ class _UISettingsWidgetState extends State<UISettingsWidget>
final titleStyle = theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
color: scheme.onSurface,
color: scheme.primary,
);
return SettingsList(
shrinkWrap: false,
shrinkWrap: true,
platform: DevicePlatform.web,
lightTheme: settingsTheme,
darkTheme: settingsTheme,