diff --git a/lib/features/admin/admin_screen.dart b/lib/features/admin/admin_screen.dart index 91d30ac..2ab3952 100644 --- a/lib/features/admin/admin_screen.dart +++ b/lib/features/admin/admin_screen.dart @@ -3238,6 +3238,18 @@ class _AdminScreenState extends ConsumerState { 'Offset Hijriah ${_hijriOffsetDays >= 0 ? '+' : ''}$_hijriOffsetDays hari tersimpan', ); }, + onProgressChanged: (nextProgress) { + final mapped = (minOffset + ((maxOffset - minOffset) * nextProgress)).round(); + final clamped = mapped.clamp(minOffset, maxOffset).toInt(); + if (clamped == _hijriOffsetDays) return; + setState(() { + _hijriOffsetDays = clamped; + }); + _queueJadwalAutoSave( + message: + 'Offset Hijriah ${_hijriOffsetDays >= 0 ? '+' : ''}$_hijriOffsetDays hari tersimpan', + ); + }, ); } @@ -3361,6 +3373,8 @@ class _AdminScreenState extends ConsumerState { }) { final value = _parseCtrlInt(controller, fallback); final valueLabel = suffix.isEmpty ? '$value' : '$value $suffix'; + final denominator = max - min; + final progress = denominator <= 0 ? 0.0 : ((value - min) / denominator); return _scrollAware( controller: _scrollControllerForTab(_selectedTab), @@ -3369,7 +3383,7 @@ class _AdminScreenState extends ConsumerState { focusNode: focusNode, label: label, valueLabel: valueLabel, - progress: ((value - min) / (max - min)).clamp(0.0, 1.0), + progress: progress.clamp(0.0, 1.0), helperText: 'Tekan OK untuk mulai ubah. Saat aktif, gunakan ← → untuk menyesuaikan nilai.', onMoveLeft: onMoveLeft, onMoveUp: onMoveUp, @@ -3394,6 +3408,19 @@ class _AdminScreenState extends ConsumerState { ); onValueChanged?.call(); }, + onProgressChanged: denominator <= 0 + ? null + : (nextProgress) { + final mapped = (min + ((max - min) * nextProgress)) + .round() + .clamp(min, max) + .toInt(); + if (mapped == _parseCtrlInt(controller, fallback)) return; + setState(() { + controller.text = mapped.toString(); + }); + onValueChanged?.call(); + }, ), ); } @@ -3410,6 +3437,7 @@ class _AdminScreenState extends ConsumerState { VoidCallback? onMoveDown, required VoidCallback onIncrement, required VoidCallback onDecrement, + ValueChanged? onProgressChanged, }) { return _TvAdjustTile( scale: s, @@ -3423,6 +3451,7 @@ class _AdminScreenState extends ConsumerState { onMoveDown: onMoveDown, onIncrement: onIncrement, onDecrement: onDecrement, + onProgressChanged: onProgressChanged, ); } @@ -3524,6 +3553,11 @@ class _AdminScreenState extends ConsumerState { onMoveDown: onMoveDown, onIncrement: () => onChanged((value + step).clamp(0.5, 2.0)), onDecrement: () => onChanged((value - step).clamp(0.5, 2.0)), + onProgressChanged: (nextProgress) { + final mapped = (0.5 + (1.5 * nextProgress)).clamp(0.5, 2.0); + final snapped = (((mapped / step).round() * step).clamp(0.5, 2.0)).toDouble(); + onChanged(snapped); + }, ); } @@ -4185,6 +4219,7 @@ class _TvAdjustTile extends StatefulWidget { final VoidCallback? onMoveDown; final VoidCallback onIncrement; final VoidCallback onDecrement; + final ValueChanged? onProgressChanged; const _TvAdjustTile({ required this.scale, @@ -4198,6 +4233,7 @@ class _TvAdjustTile extends StatefulWidget { this.onMoveDown, required this.onIncrement, required this.onDecrement, + this.onProgressChanged, }); @override @@ -4243,6 +4279,12 @@ class _TvAdjustTileState extends State<_TvAdjustTile> { FocusNode get _focusNode => widget.focusNode ?? _fallbackFocusNode; + void _updateFromTouchPosition(double x, double width) { + if (widget.onProgressChanged == null || width <= 0) return; + final normalized = (x / width).clamp(0.0, 1.0); + widget.onProgressChanged!(normalized); + } + @override void dispose() { if (widget.focusNode == null) { @@ -4385,72 +4427,114 @@ class _TvAdjustTileState extends State<_TvAdjustTile> { SizedBox(height: 14 * s), Row( children: [ - Container( - width: 36 * s, - height: 36 * s, - alignment: Alignment.center, - decoration: BoxDecoration( - color: _isEditing - ? SacredColors.surfaceContainerHigh - : SacredColors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(SacredRadii.sm), - border: Border.all( - color: _isEditing - ? SacredColors.primary.withValues(alpha: 0.8) - : SacredColors.outlineVariant.withValues(alpha: 0.35), - ), - ), - child: Text( - '←', - style: GoogleFonts.manrope( - fontSize: 16 * s, - fontWeight: FontWeight.w800, - color: SacredColors.onSurface, - ), - ), - ), - SizedBox(width: 12 * s), - Expanded( + GestureDetector( + onTap: () { + _focusNode.requestFocus(); + widget.onDecrement(); + }, child: Container( - height: 6 * s, + width: 36 * s, + height: 36 * s, + alignment: Alignment.center, decoration: BoxDecoration( - color: SacredColors.outlineVariant.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(3 * s), + color: _isEditing + ? SacredColors.surfaceContainerHigh + : SacredColors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(SacredRadii.sm), + border: Border.all( + color: _isEditing + ? SacredColors.primary.withValues(alpha: 0.8) + : SacredColors.outlineVariant.withValues(alpha: 0.35), + ), ), - child: FractionallySizedBox( - alignment: Alignment.centerLeft, - widthFactor: widget.progress.clamp(0.0, 1.0), - child: Container( - decoration: BoxDecoration( - color: SacredColors.primary, - borderRadius: BorderRadius.circular(3 * s), - ), + child: Text( + '←', + style: GoogleFonts.manrope( + fontSize: 16 * s, + fontWeight: FontWeight.w800, + color: SacredColors.onSurface, ), ), ), ), SizedBox(width: 12 * s), - Container( - width: 36 * s, - height: 36 * s, - alignment: Alignment.center, - decoration: BoxDecoration( - color: _isEditing - ? SacredColors.surfaceContainerHigh - : SacredColors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(SacredRadii.sm), - border: Border.all( - color: _isEditing - ? SacredColors.primary.withValues(alpha: 0.8) - : SacredColors.outlineVariant.withValues(alpha: 0.35), - ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final barWidth = constraints.maxWidth; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTapDown: (details) { + _focusNode.requestFocus(); + _updateFromTouchPosition(details.localPosition.dx, barWidth); + }, + onHorizontalDragStart: widget.onProgressChanged == null + ? null + : (_) { + _focusNode.requestFocus(); + setState(() => _isEditing = true); + }, + onHorizontalDragUpdate: widget.onProgressChanged == null + ? null + : (details) => _updateFromTouchPosition( + details.localPosition.dx, + barWidth, + ), + onHorizontalDragEnd: widget.onProgressChanged == null + ? null + : (_) => setState(() => _isEditing = false), + onHorizontalDragCancel: widget.onProgressChanged == null + ? null + : () => setState(() => _isEditing = false), + child: Container( + height: 6 * s, + decoration: BoxDecoration( + color: SacredColors.outlineVariant.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(3 * s), + ), + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: widget.progress.clamp(0.0, 1.0), + child: Container( + decoration: BoxDecoration( + color: SacredColors.primary, + borderRadius: BorderRadius.circular(3 * s), + ), + ), + ), + ), + ); + }, ), - child: Text( - '→', - style: GoogleFonts.manrope( - fontSize: 16 * s, - fontWeight: FontWeight.w800, - color: SacredColors.onSurface, + ), + SizedBox(width: 12 * s), + GestureDetector( + onTap: () { + _focusNode.requestFocus(); + widget.onIncrement(); + }, + child: Container( + width: 36 * s, + height: 36 * s, + alignment: Alignment.center, + decoration: BoxDecoration( + color: _isEditing + ? SacredColors.surfaceContainerHigh + : SacredColors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(SacredRadii.sm), + border: Border.all( + color: _isEditing + ? SacredColors.primary.withValues(alpha: 0.8) + : SacredColors.outlineVariant.withValues(alpha: 0.35), + ), + ), + child: Text( + '→', + style: GoogleFonts.manrope( + fontSize: 16 * s, + fontWeight: FontWeight.w800, + color: SacredColors.onSurface, + ), ), ), ),