feat(tv-ui): add slideshow pattern mode and improve admin readability
This commit is contained in:
@@ -9,6 +9,7 @@ import 'package:intl/intl.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../core/sacred_tokens.dart';
|
||||
import '../../data/local/models.dart';
|
||||
import '../../providers.dart';
|
||||
import '../../data/services/sync_service.dart';
|
||||
import '../../data/services/myquran_service.dart';
|
||||
@@ -39,12 +40,14 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
|
||||
final _mainDurCtrl = TextEditingController();
|
||||
final _slideDurCtrl = TextEditingController();
|
||||
final _slidesPerMainCtrl = TextEditingController();
|
||||
final _mainHeroDurCtrl = TextEditingController();
|
||||
final _textSlideDurCtrl = TextEditingController();
|
||||
|
||||
int _selectedTab = 0;
|
||||
bool _isSyncing = false;
|
||||
int _textScaleIndex = 1;
|
||||
String _slideshowPatternMode = SlideshowPatternMode.alternating;
|
||||
List<String> _slideshowImages = [];
|
||||
bool _useUnsplash = false;
|
||||
final _unsplashKeywordCtrl = TextEditingController();
|
||||
@@ -162,9 +165,11 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
_cityCtrl.text = '${settings.cityDisplayName} (${settings.cityIdApi})';
|
||||
_mainDurCtrl.text = settings.mainScreenDurationSec.toString();
|
||||
_slideDurCtrl.text = settings.slideDurationSec.toString();
|
||||
_slidesPerMainCtrl.text = settings.slideshowSlidesPerMain.toString();
|
||||
_mainHeroDurCtrl.text = settings.mainCenterSlideDurationSec.toString();
|
||||
_textSlideDurCtrl.text = settings.announcementSlideDurationSec.toString();
|
||||
_textScaleIndex = settings.textScaleIndex;
|
||||
_slideshowPatternMode = settings.slideshowPatternMode;
|
||||
_slideshowImages = List.from(settings.slideshowImages);
|
||||
_useUnsplash = settings.useUnsplashBackground;
|
||||
_unsplashKeywordCtrl.text = settings.unsplashKeyword;
|
||||
@@ -202,6 +207,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
|
||||
_mainDurCtrl.addListener(_queueTampilanAutoSave);
|
||||
_slideDurCtrl.addListener(_queueTampilanAutoSave);
|
||||
_slidesPerMainCtrl.addListener(_queueTampilanAutoSave);
|
||||
_mainHeroDurCtrl.addListener(_queuePengumumanAutoSave);
|
||||
_textSlideDurCtrl.addListener(_queuePengumumanAutoSave);
|
||||
_unsplashKeywordCtrl.addListener(_queueTampilanAutoSave);
|
||||
@@ -236,6 +242,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
_cityCtrl.dispose();
|
||||
_mainDurCtrl.dispose();
|
||||
_slideDurCtrl.dispose();
|
||||
_slidesPerMainCtrl.dispose();
|
||||
_mainHeroDurCtrl.dispose();
|
||||
_textSlideDurCtrl.dispose();
|
||||
_unsplashKeywordCtrl.dispose();
|
||||
@@ -322,6 +329,13 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
s.slideshowImages = List.from(_slideshowImages);
|
||||
s.mainScreenDurationSec = int.tryParse(_mainDurCtrl.text.trim()) ?? 15;
|
||||
s.slideDurationSec = int.tryParse(_slideDurCtrl.text.trim()) ?? 10;
|
||||
s.slideshowPatternMode = SlideshowPatternMode.isValid(_slideshowPatternMode)
|
||||
? _slideshowPatternMode
|
||||
: SlideshowPatternMode.alternating;
|
||||
s.slideshowSlidesPerMain =
|
||||
(int.tryParse(_slidesPerMainCtrl.text.trim()) ?? 2)
|
||||
.clamp(1, 20)
|
||||
.toInt();
|
||||
s.useUnsplashBackground = _useUnsplash;
|
||||
s.unsplashKeyword = _unsplashKeywordCtrl.text.trim().isEmpty ? 'mosque' : _unsplashKeywordCtrl.text.trim();
|
||||
s.unsplashRotationHours = int.tryParse(_unsplashRotationCtrl.text.trim()) ?? 6;
|
||||
@@ -1390,15 +1404,16 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
|
||||
int _tampilanRowCount() {
|
||||
var count = 0;
|
||||
count += 8;
|
||||
count += 11;
|
||||
if (_slideshowPatternMode == SlideshowPatternMode.burst) {
|
||||
count += 1;
|
||||
}
|
||||
if (_useUnsplash) {
|
||||
count += 2;
|
||||
}
|
||||
if (_brandedBgImage != null && _brandedBgImage!.isNotEmpty) {
|
||||
count += 1;
|
||||
}
|
||||
count += 1;
|
||||
count += 1;
|
||||
count += _slideshowImages.length;
|
||||
return count;
|
||||
}
|
||||
@@ -1844,7 +1859,15 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
children: [
|
||||
Text(title, style: GoogleFonts.manrope(fontSize: 15 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurface)),
|
||||
SizedBox(height: 4 * s),
|
||||
Text(desc, style: GoogleFonts.manrope(fontSize: 13 * s, color: SacredColors.onSurfaceVariant)),
|
||||
Text(
|
||||
desc,
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 15 * s,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: SacredColors.onSurfaceVariant,
|
||||
height: 1.35,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1857,6 +1880,11 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
final textScaleRow = row++;
|
||||
final mainDurationRow = row++;
|
||||
final slideDurationRow = row++;
|
||||
final slideshowPatternRow = row++;
|
||||
int? slidesPerMainRow;
|
||||
if (_slideshowPatternMode == SlideshowPatternMode.burst) {
|
||||
slidesPerMainRow = row++;
|
||||
}
|
||||
final scaleLabelRow = row++;
|
||||
final scaleBodyRow = row++;
|
||||
final scaleRunningRow = row++;
|
||||
@@ -1942,8 +1970,49 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
suffix: 'detik',
|
||||
onMoveLeft: () => _focusNavTab(_selectedTab),
|
||||
onMoveUp: () => _focusTampilanRow(mainDurationRow),
|
||||
onMoveDown: () => _focusTampilanRow(scaleLabelRow),
|
||||
onMoveDown: () => _focusTampilanRow(slideshowPatternRow),
|
||||
),
|
||||
SizedBox(height: 24 * s),
|
||||
_buildTvChoiceField(
|
||||
s: s,
|
||||
rowIndex: slideshowPatternRow,
|
||||
label: 'Pola Rotasi Slideshow',
|
||||
options: const ['Main-1-Main', 'Main-N-Main'],
|
||||
selectedIndex:
|
||||
_slideshowPatternMode == SlideshowPatternMode.burst
|
||||
? 1
|
||||
: 0,
|
||||
onChanged: (index) {
|
||||
setState(() {
|
||||
_slideshowPatternMode = index == 1
|
||||
? SlideshowPatternMode.burst
|
||||
: SlideshowPatternMode.alternating;
|
||||
if (_slideshowPatternMode == SlideshowPatternMode.burst &&
|
||||
_slidesPerMainCtrl.text.trim().isEmpty) {
|
||||
_slidesPerMainCtrl.text = '2';
|
||||
}
|
||||
});
|
||||
_queueTampilanAutoSave(
|
||||
message: 'Pola slideshow otomatis tersimpan',
|
||||
);
|
||||
},
|
||||
),
|
||||
if (_slideshowPatternMode == SlideshowPatternMode.burst) ...[
|
||||
SizedBox(height: 16 * s),
|
||||
_buildTvIntStepperField(
|
||||
s: s,
|
||||
label: 'Jumlah Slide antar Main',
|
||||
focusNode: _tampilanFocusNode(slidesPerMainRow!),
|
||||
controller: _slidesPerMainCtrl,
|
||||
fallback: 2,
|
||||
min: 1,
|
||||
max: 20,
|
||||
suffix: 'slide',
|
||||
onMoveLeft: () => _focusNavTab(_selectedTab),
|
||||
onMoveUp: () => _focusTampilanRow(slideshowPatternRow),
|
||||
onMoveDown: () => _focusTampilanRow(scaleLabelRow),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 40 * s),
|
||||
_sectionLabel('Ukuran Teks Per Kelompok', s),
|
||||
SizedBox(height: 8 * s),
|
||||
@@ -1962,7 +2031,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
_queueTampilanAutoSave();
|
||||
},
|
||||
onMoveLeft: () => _focusNavTab(_selectedTab),
|
||||
onMoveUp: () => _focusTampilanRow(slideDurationRow),
|
||||
onMoveUp: () => _focusTampilanRow(
|
||||
slidesPerMainRow ?? slideshowPatternRow,
|
||||
),
|
||||
onMoveDown: () => _focusTampilanRow(scaleBodyRow),
|
||||
),
|
||||
SizedBox(height: 16 * s),
|
||||
@@ -3375,7 +3446,8 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
return Text(
|
||||
label,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 20 * s,
|
||||
// Match sidebar menu text size for stronger hierarchy consistency.
|
||||
fontSize: 18 * s,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: SacredColors.primary,
|
||||
),
|
||||
@@ -4249,8 +4321,22 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: GoogleFonts.manrope(fontSize: 12 * s, color: SacredColors.onSurfaceVariant)),
|
||||
Text(value, style: GoogleFonts.plusJakartaSans(fontSize: 18 * s, fontWeight: FontWeight.w600, color: SacredColors.onSurface)),
|
||||
Text(
|
||||
label,
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 15 * s,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: SacredColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 20 * s,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: SacredColors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -4334,7 +4420,12 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
SizedBox(height: 16 * s),
|
||||
Text(
|
||||
'Gunakan tombol di bawah ini untuk melihat pratinjau bagaimana aplikasi bereaksi terhadap berbagai waktu dan status tanpa harus menunggu waktu sebenarnya.\nFitur ini bekerja dengan menggeser waktu aplikasi (Time Travel).',
|
||||
style: GoogleFonts.manrope(fontSize: 18 * s, color: SacredColors.onSurfaceVariant),
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 20 * s,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.35,
|
||||
color: SacredColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 48 * s),
|
||||
Container(
|
||||
@@ -4524,7 +4615,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
Text(
|
||||
'Informasi aplikasi, kontak bantuan, dan pemeriksaan versi terbaru.',
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 18 * s,
|
||||
fontSize: 20 * s,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.35,
|
||||
color: SacredColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
@@ -4834,7 +4927,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
Text(
|
||||
desc,
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 14 * s,
|
||||
fontSize: 16 * s,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.35,
|
||||
color: SacredColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
@@ -5175,10 +5270,11 @@ class _TvAdjustTileState extends State<_TvAdjustTile> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.label,
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 16 * s,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: SacredColors.onSurface,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 18 * s,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: SacredColors.onSurfaceVariant,
|
||||
letterSpacing: 0.4 * s,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -5327,8 +5423,9 @@ class _TvAdjustTileState extends State<_TvAdjustTile> {
|
||||
? 'Mode ubah aktif. Gunakan ← → lalu tekan OK untuk selesai.'
|
||||
: widget.helperText,
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 11 * s,
|
||||
color: SacredColors.onSurfaceVariant.withValues(alpha: 0.75),
|
||||
fontSize: 15 * s,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: SacredColors.onSurfaceVariant.withValues(alpha: 0.88),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -5514,10 +5611,11 @@ class _TvEditableTextTileState extends State<_TvEditableTextTile> {
|
||||
children: [
|
||||
Text(
|
||||
widget.label,
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 16 * s,
|
||||
fontWeight: FontWeight.w600,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 18 * s,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: SacredColors.onSurfaceVariant,
|
||||
letterSpacing: 0.4 * s,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12 * s),
|
||||
@@ -5565,8 +5663,9 @@ class _TvEditableTextTileState extends State<_TvEditableTextTile> {
|
||||
? 'Mode edit aktif. Tekan ESC untuk selesai.'
|
||||
: 'Tekan OK untuk mulai edit.',
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 11 * s,
|
||||
color: SacredColors.onSurfaceVariant.withValues(alpha: 0.75),
|
||||
fontSize: 15 * s,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: SacredColors.onSurfaceVariant.withValues(alpha: 0.88),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user