diff --git a/refilc/lib/api/client.dart b/refilc/lib/api/client.dart index 5fcaf4fa..a8ddedd9 100644 --- a/refilc/lib/api/client.dart +++ b/refilc/lib/api/client.dart @@ -304,8 +304,9 @@ class FilcAPI { } } on Exception catch (error, stacktrace) { log("ERROR: FilcAPI.getAllSharedThemes: $error $stacktrace"); + // return empty array if error + return []; } - return null; } static Future addSharedGradeColors(SharedGradeColors gradeColors) async { diff --git a/refilc_kreta_api/lib/providers/share_provider.dart b/refilc_kreta_api/lib/providers/share_provider.dart index c45c2284..2a4e3512 100644 --- a/refilc_kreta_api/lib/providers/share_provider.dart +++ b/refilc_kreta_api/lib/providers/share_provider.dart @@ -7,6 +7,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:uuid/uuid.dart'; +List? _cachedThemesJson; + class ShareProvider extends ChangeNotifier { final UserProvider _user; @@ -101,50 +103,59 @@ class ShareProvider extends ChangeNotifier { return null; } - Future> getAllPublicThemes(BuildContext context, - {int count = 0}) async { - List? themesJson = await FilcAPI.getAllSharedThemes(count); +Future> getAllPublicThemes(BuildContext context, + {int offset = 0, int limit = 10}) async { + // Fetch all themes only once if not already fetched + if (_cachedThemesJson == null) { + _cachedThemesJson = await FilcAPI.getAllSharedThemes(0); + } - List themes = []; + List themes = []; - if (themesJson != null) { - for (var t in themesJson) { - if (t['public_id'].toString().replaceAll(' ', '') == '') continue; - if (t['grade_colors_id'].toString().replaceAll(' ', '') == '') continue; + if (_cachedThemesJson != null) { + // Get the current chunk based on offset and limit + final int endIndex = (offset + limit <= _cachedThemesJson!.length) + ? offset + limit + : _cachedThemesJson!.length; + final chunk = _cachedThemesJson!.sublist(offset, endIndex); - Map? gradeColorsJson = - await FilcAPI.getSharedGradeColors(t['grade_colors_id']); + for (var t in chunk) { + if (t['public_id'].toString().replaceAll(' ', '') == '') continue; + if (t['grade_colors_id'].toString().replaceAll(' ', '') == '') continue; - if (gradeColorsJson != null) { - SharedTheme theme = SharedTheme.fromJson( - t, - SharedGradeColors.fromJson(gradeColorsJson["public_id"] != '' - ? gradeColorsJson - : { - "public_id": "0", - "is_public": false, - "nickname": "Anonymous", - "five_color": - SettingsProvider.defaultSettings().gradeColors[4].value, - "four_color": - SettingsProvider.defaultSettings().gradeColors[3].value, - "three_color": - SettingsProvider.defaultSettings().gradeColors[2].value, - "two_color": - SettingsProvider.defaultSettings().gradeColors[1].value, - "one_color": - SettingsProvider.defaultSettings().gradeColors[0].value, - }), - ); + Map? gradeColorsJson = + await FilcAPI.getSharedGradeColors(t['grade_colors_id']); - themes.add(theme); - } + if (gradeColorsJson != null) { + SharedTheme theme = SharedTheme.fromJson( + t, + SharedGradeColors.fromJson(gradeColorsJson["public_id"] != '' + ? gradeColorsJson + : { + "public_id": "0", + "is_public": false, + "nickname": "Anonymous", + "five_color": + SettingsProvider.defaultSettings().gradeColors[4].value, + "four_color": + SettingsProvider.defaultSettings().gradeColors[3].value, + "three_color": + SettingsProvider.defaultSettings().gradeColors[2].value, + "two_color": + SettingsProvider.defaultSettings().gradeColors[1].value, + "one_color": + SettingsProvider.defaultSettings().gradeColors[0].value, + }), + ); + + themes.add(theme); } } - - return themes; } + return themes; +} + // grade colors Future<(SharedGradeColors?, int)> shareCurrentGradeColors( BuildContext context, { diff --git a/refilc_mobile_ui/lib/common/empty.dart b/refilc_mobile_ui/lib/common/empty.dart index a306a8d2..5179a6c6 100644 --- a/refilc_mobile_ui/lib/common/empty.dart +++ b/refilc_mobile_ui/lib/common/empty.dart @@ -9,12 +9,11 @@ List faces = [ "(o^▽^o)", "(⌒▽⌒)☆", "( ̄ω ̄)", - "(≧◡≦)", - "(◕‿◕)", - "(⌒‿⌒)", + "(◕ω◕)", + "(⌒ω⌒)", "(☆ω☆)", "(^ヮ^)/", - "(⁀ᗢ⁀)", + "(^ᗢ^)", "(≧ω≦)", "(/ω\)", "(//▽//)", @@ -26,10 +25,8 @@ List faces = [ "(・ω・)ノ", "(=^・ω・^=)", "( =ω= )", - "( ͡° ͜ʖ ͡°)", - "( ͡• ͜ʖ ͡• )", - "(◕‿◕✿)", - "(✿◠‿◠)" + "(◕ω◕✿)", + "(✿◠ω◠)" ]; class Empty extends StatelessWidget { diff --git a/refilc_mobile_ui/lib/screens/settings/submenu/paint_list.dart b/refilc_mobile_ui/lib/screens/settings/submenu/paint_list.dart index 1df4087d..e2c060e0 100644 --- a/refilc_mobile_ui/lib/screens/settings/submenu/paint_list.dart +++ b/refilc_mobile_ui/lib/screens/settings/submenu/paint_list.dart @@ -31,13 +31,9 @@ class MenuPaintList extends StatelessWidget { @override Widget build(BuildContext context) { return PanelButton( - onPressed: () async { - List publicThemes = - await Provider.of(context, listen: false) - .getAllPublicThemes(context); - - Navigator.of(context, rootNavigator: true).push(CupertinoPageRoute( - builder: (context) => PaintListScreen(publicThemes: publicThemes))); + onPressed: () { + Navigator.of(context, rootNavigator: true).push( + CupertinoPageRoute(builder: (context) => const PaintListScreen())); }, title: Text( "own_paints".i18n, @@ -61,9 +57,7 @@ class MenuPaintList extends StatelessWidget { } class PaintListScreen extends StatefulWidget { - const PaintListScreen({super.key, required this.publicThemes}); - - final List publicThemes; + const PaintListScreen({super.key}); @override PaintListScreenState createState() => PaintListScreenState(); @@ -74,42 +68,80 @@ class PaintListScreenState extends State late SettingsProvider settingsProvider; late UserProvider user; late ShareProvider shareProvider; - late AnimationController _hideContainersController; - - late List tiles; - final _paintId = TextEditingController(); SharedTheme? newThemeByID; + List publicThemes = []; + List tiles = []; + bool isLoading = false; + bool hasMore = true; + int currentOffset = 0; + static const int pageSize = 10; + + final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); - shareProvider = Provider.of(context, listen: false); - _hideContainersController = AnimationController( vsync: this, duration: const Duration(milliseconds: 200)); + + _scrollController.addListener(_scrollListener); + _loadMoreThemes(); } - void buildPublicPaintTiles() async { - List subjectTiles = []; + @override + void dispose() { + _scrollController.dispose(); + _hideContainersController.dispose(); + super.dispose(); + } - var added = []; - var i = 0; + void _scrollListener() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent * 0.8) { + _loadMoreThemes(); + } + } - for (var t in widget.publicThemes) { - if (added.contains(t.id)) continue; + Future _loadMoreThemes() async { + if (isLoading || !hasMore) return; - Widget w = PanelButton( + setState(() { + isLoading = true; + }); + + try { + final newThemes = await shareProvider.getAllPublicThemes( + context, + offset: currentOffset, + limit: pageSize, + ); + + setState(() { + publicThemes.addAll(newThemes); + currentOffset += newThemes.length; + hasMore = newThemes.length == pageSize; + isLoading = false; + }); + } catch (e) { + setState(() { + isLoading = false; + }); + } + } + + void buildPublicPaintTiles() { + tiles = []; // Clear existing tiles + + for (var i = 0; i < publicThemes.length; i++) { + final t = publicThemes[i]; + tiles.add(PanelButton( onPressed: () async { newThemeByID = t; - - // slay - setPaint(); - setState(() {}); }, title: Column( @@ -162,231 +194,24 @@ class PaintListScreenState extends State ), borderRadius: BorderRadius.vertical( top: Radius.circular(i == 0 ? 12.0 : 4.0), - bottom: - Radius.circular(i + 1 == widget.publicThemes.length ? 12.0 : 4.0), + bottom: Radius.circular( + i + 1 == publicThemes.length && !hasMore ? 12.0 : 4.0), ), - ); - - i += 1; - subjectTiles.add(w); - added.add(t.id); - } - - if (widget.publicThemes.isEmpty) { - subjectTiles.add(Empty( - subtitle: 'no_pub_paint'.i18n, )); } - tiles = subjectTiles; - } - - @override - Widget build(BuildContext context) { - settingsProvider = Provider.of(context); - user = Provider.of(context); - - buildPublicPaintTiles(); - - return AnimatedBuilder( - animation: _hideContainersController, - builder: (context, child) => Opacity( - opacity: 1 - _hideContainersController.value, - child: Scaffold( - appBar: AppBar( - surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, - leading: BackButton(color: AppColors.of(context).text), - title: Text( - "own_paints".i18n, - style: TextStyle(color: AppColors.of(context).text), - ), - ), - body: SingleChildScrollView( - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), - child: Column( - children: [ - // enter id - SplittedPanel( - padding: EdgeInsets.zero, - cardPadding: const EdgeInsets.all(3.0), - hasBorder: true, - isTransparent: true, - children: [ - PanelButton( - onPressed: () => showEnterIDDialog(), - title: Text( - "enter_id".i18n, - style: TextStyle( - color: AppColors.of(context) - .text - .withValues(alpha: .95), - ), - ), - leading: Icon( - FeatherIcons.plus, - size: 22.0, - color: - AppColors.of(context).text.withValues(alpha: .95), - ), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12.0), - bottom: Radius.circular(12.0), - ), - ), - ], - ), - - const SizedBox( - height: 18.0, - ), - // current paint - SplittedPanel( - title: Text('current_paint'.i18n), - padding: EdgeInsets.zero, - cardPadding: const EdgeInsets.all(4.0), - children: [ - PanelButton( - onPressed: () async { - if (settingsProvider.currentThemeId != '') { - Share.share( - settingsProvider.currentThemeId, - subject: 'share_subj_theme'.i18n, - ); - } else { - ShareThemeDialog.show(context); - } - }, - longPressInstead: true, - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - settingsProvider.currentThemeDisplayName != '' - ? settingsProvider.currentThemeDisplayName - : 'no_name'.i18n, - style: TextStyle( - color: AppColors.of(context) - .text - .withValues(alpha: .95), - ), - ), - Text( - settingsProvider.currentThemeCreator != '' - ? settingsProvider.currentThemeCreator - : 'Anonymous', - style: TextStyle( - color: AppColors.of(context) - .text - .withValues(alpha: .65), - fontSize: 15.0, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - trailing: Transform.translate( - offset: const Offset(8.0, 0.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - margin: const EdgeInsets.only(left: 2.0), - width: 14.0, - height: 14.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: - (settingsProvider.customBackgroundColor ?? - SettingsProvider.defaultSettings() - .customBackgroundColor), - boxShadow: [ - BoxShadow( - color: AppColors.of(context) - .text - .withValues(alpha: 0.15), - offset: const Offset(1, 2), - blurRadius: 3, - ), - ], - ), - ), - Transform.translate( - offset: const Offset(-4.0, 0.0), - child: Container( - margin: const EdgeInsets.only(left: 2.0), - width: 14.0, - height: 14.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: (settingsProvider - .customHighlightColor ?? - SettingsProvider.defaultSettings() - .customHighlightColor), - boxShadow: [ - BoxShadow( - color: AppColors.of(context) - .text - .withValues(alpha: 0.15), - offset: const Offset(1, 2), - blurRadius: 3, - ), - ], - ), - ), - ), - Transform.translate( - offset: const Offset(-8.0, 0.0), - child: Container( - margin: const EdgeInsets.only(left: 2.0), - width: 14.0, - height: 14.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: settingsProvider.customAccentColor ?? - accentColorMap[ - settingsProvider.accentColor], - boxShadow: [ - BoxShadow( - color: AppColors.of(context) - .text - .withValues(alpha: 0.15), - offset: const Offset(1, 2), - blurRadius: 3, - ), - ], - ), - ), - ), - ], - ), - ), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12), - bottom: Radius.circular(12), - ), - ), - ], - ), - - const SizedBox( - height: 18.0, - ), - // own paints - SplittedPanel( - title: Text('public_paint'.i18n), - padding: EdgeInsets.zero, - cardPadding: const EdgeInsets.all(4.0), - children: tiles, - ), - ], - ), - ), - ), + if (isLoading) { + tiles.add(const Center( + child: Padding( + padding: EdgeInsets.all(8.0), + child: CircularProgressIndicator(), ), - ), - ); + )); + } + + if (publicThemes.isEmpty && !isLoading) { + tiles.add(Empty(subtitle: 'no_pub_paint'.i18n, alwaysRandom: true)); + } } // enter id dialog @@ -573,4 +398,185 @@ class PaintListScreenState extends State Provider.of(context, listen: false) .changeTheme(settingsProvider.theme, updateNavbarColor: true); } + +@override +Widget build(BuildContext context) { + settingsProvider = Provider.of(context); + user = Provider.of(context); + + buildPublicPaintTiles(); + + return AnimatedBuilder( + animation: _hideContainersController, + builder: (context, child) => Opacity( + opacity: 1 - _hideContainersController.value, + child: Scaffold( + appBar: AppBar( + surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, + leading: BackButton(color: AppColors.of(context).text), + title: Text( + "own_paints".i18n, + style: TextStyle(color: AppColors.of(context).text), + ), + ), + body: SingleChildScrollView( + controller: _scrollController, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), + child: Column( + children: [ + SplittedPanel( + padding: EdgeInsets.zero, + cardPadding: const EdgeInsets.all(3.0), + hasBorder: true, + isTransparent: true, + children: [ + PanelButton( + onPressed: () => showEnterIDDialog(), + title: Text( + "enter_id".i18n, + style: TextStyle( + color: AppColors.of(context).text.withValues(alpha: .95), + ), + ), + leading: Icon( + FeatherIcons.plus, + size: 22.0, + color: AppColors.of(context).text.withValues(alpha: .95), + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12.0), + bottom: Radius.circular(12.0), + ), + ), + ], + ), + const SizedBox(height: 18.0), + SplittedPanel( + title: Text('current_paint'.i18n), + padding: EdgeInsets.zero, + cardPadding: const EdgeInsets.all(4.0), + children: [ + PanelButton( + onPressed: () async { + if (settingsProvider.currentThemeId != '') { + Share.share( + settingsProvider.currentThemeId, + subject: 'share_subj_theme'.i18n, + ); + } else { + ShareThemeDialog.show(context); + } + }, + longPressInstead: true, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + settingsProvider.currentThemeDisplayName != '' + ? settingsProvider.currentThemeDisplayName + : 'no_name'.i18n, + style: TextStyle( + color: AppColors.of(context).text.withValues(alpha: .95), + ), + ), + Text( + settingsProvider.currentThemeCreator != '' + ? settingsProvider.currentThemeCreator + : 'Anonymous', + style: TextStyle( + color: AppColors.of(context).text.withValues(alpha: .65), + fontSize: 15.0, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + trailing: Transform.translate( + offset: const Offset(8.0, 0.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: const EdgeInsets.only(left: 2.0), + width: 14.0, + height: 14.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: (settingsProvider.customBackgroundColor ?? + SettingsProvider.defaultSettings().customBackgroundColor), + boxShadow: [ + BoxShadow( + color: AppColors.of(context).text.withValues(alpha: 0.15), + offset: const Offset(1, 2), + blurRadius: 3, + ), + ], + ), + ), + Transform.translate( + offset: const Offset(-4.0, 0.0), + child: Container( + margin: const EdgeInsets.only(left: 2.0), + width: 14.0, + height: 14.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: (settingsProvider.customHighlightColor ?? + SettingsProvider.defaultSettings().customHighlightColor), + boxShadow: [ + BoxShadow( + color: AppColors.of(context).text.withValues(alpha: 0.15), + offset: const Offset(1, 2), + blurRadius: 3, + ), + ], + ), + ), + ), + Transform.translate( + offset: const Offset(-8.0, 0.0), + child: Container( + margin: const EdgeInsets.only(left: 2.0), + width: 14.0, + height: 14.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: settingsProvider.customAccentColor ?? + accentColorMap[settingsProvider.accentColor], + boxShadow: [ + BoxShadow( + color: AppColors.of(context).text.withValues(alpha: 0.15), + offset: const Offset(1, 2), + blurRadius: 3, + ), + ], + ), + ), + ), + ], + ), + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + bottom: Radius.circular(12), + ), + ), + ], + ), + const SizedBox(height: 18.0), + SplittedPanel( + title: Text('public_paint'.i18n), + padding: EdgeInsets.zero, + cardPadding: const EdgeInsets.all(4.0), + children: tiles, + ), + ], + ), + ), + ), + ), + ), + ); +} }