fix: fix explore page

feat: added placeholder for empty page
fix: small toggle theme issue
feat: replaced by Cinemeta with Madari Catalog
This commit is contained in:
omkar 2025-02-01 10:36:49 +05:30
parent 0d7573ee22
commit a38b3d1235
12 changed files with 184 additions and 102 deletions

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:madari_client/features/streamio_addons/service/stremio_addon_service.dart';
import 'package:pocketbase/pocketbase.dart';
import '../../common/utils/error_handler.dart';
@ -37,11 +38,6 @@ class _SignInPageState extends State<SignInPage>
super.initState();
_setupAnimations();
_animationController.forward();
// Set initial focus to email field
WidgetsBinding.instance.addPostFrameCallback((_) {
FocusScope.of(context).requestFocus(_emailFocusNode);
});
}
void _setupAnimations() {
@ -91,81 +87,58 @@ class _SignInPageState extends State<SignInPage>
),
);
},
child: RawKeyboardListener(
focusNode: FocusNode(),
onKey: (RawKeyEvent event) {
if (event is RawKeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
if (focusNode == _emailFocusNode) {
FocusScope.of(context).requestFocus(_passwordFocusNode);
} else if (focusNode == _passwordFocusNode) {
FocusScope.of(context).requestFocus(_forgotPasswordFocusNode);
}
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
if (focusNode == _passwordFocusNode) {
FocusScope.of(context).requestFocus(_emailFocusNode);
} else if (focusNode == _forgotPasswordFocusNode) {
FocusScope.of(context).requestFocus(_passwordFocusNode);
}
}
}
},
child: TextFormField(
controller: controller,
focusNode: focusNode,
obscureText: isPassword ? _obscurePassword : false,
keyboardType: keyboardType,
textInputAction: textInputAction,
autofillHints: autofillHints,
onEditingComplete: onEditingComplete,
style: Theme.of(context).textTheme.bodyLarge,
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon),
suffixIcon: isPassword
? IconButton(
focusNode: FocusNode(),
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
tooltip:
_obscurePassword ? 'Show password' : 'Hide password',
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
child: TextFormField(
controller: controller,
focusNode: focusNode,
obscureText: isPassword ? _obscurePassword : false,
keyboardType: keyboardType,
textInputAction: textInputAction,
autofillHints: autofillHints,
onEditingComplete: onEditingComplete,
style: Theme.of(context).textTheme.bodyLarge,
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon),
suffixIcon: isPassword
? IconButton(
focusNode: FocusNode(),
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
tooltip: _obscurePassword ? 'Show password' : 'Hide password',
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.5),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
),
filled: true,
fillColor: Theme.of(context).colorScheme.surface,
),
validator: validator,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color:
Theme.of(context).colorScheme.outline.withValues(alpha: 0.5),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
),
filled: true,
fillColor: Theme.of(context).colorScheme.surface,
),
validator: validator,
),
);
}
@ -482,6 +455,9 @@ class _SignInPageState extends State<SignInPage>
_passwordController.text,
);
final addons = StremioAddonService.instance.getInstalledAddons();
await addons.refetch();
if (mounted) {
context.go('/profile');
}

View file

@ -466,7 +466,7 @@ class _SignUpPageState extends State<SignUpPage>
"name": _nameController.text.trim(),
};
final user = await pocketbase.collection('users').create(body: userData);
await pocketbase.collection('users').create(body: userData);
await pocketbase.collection('users').authWithPassword(
_emailController.text.trim(),
@ -492,6 +492,9 @@ class _SignUpPageState extends State<SignUpPage>
await LayoutService.instance.addAllHomeWidgets();
final addons = StremioAddonService.instance.getInstalledAddons();
await addons.refetch();
if (mounted) {
context.go('/profile');
}

View file

@ -28,7 +28,7 @@ class _ExploreAddonState extends State<ExploreAddon> {
String? selectedId;
String? selectedGenre;
StremioManifest? selectedAddon;
static const int pageSize = 50;
static const int pageSize = 20;
final service = StremioAddonService.instance;
InfiniteQuery<List<Meta>, int>? _query;
@ -344,8 +344,10 @@ class _ExploreAddonState extends State<ExploreAddon> {
? CatalogFullView(
initialItems: const [],
prefix: "explore",
query: buildQuery(),
key: ValueKey(queryKey),
queryBuilder: () {
return buildQuery();
},
)
: const Center(
child: CircularProgressIndicator(),

View file

@ -121,7 +121,7 @@ class _AppearancePageState extends State<AppearancePage> {
onTap: () {
if (AppTheme().getCurrentTheme().brightness ==
Brightness.dark) {
setState(() => AppTheme().toggleTheme());
AppTheme().toggleTheme();
}
},
child: Column(
@ -160,7 +160,7 @@ class _AppearancePageState extends State<AppearancePage> {
onTap: () {
if (AppTheme().getCurrentTheme().brightness ==
Brightness.light) {
setState(() => AppTheme().toggleTheme());
AppTheme().toggleTheme();
}
},
child: Column(

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:madari_client/features/home/pages/home_page.dart';
import 'package:madari_client/features/settings/service/selected_profile.dart';
import 'package:madari_client/features/widgetter/plugins/stremio/utils/size.dart';
import '../../pocketbase/service/pocketbase.service.dart';
import '../../widgetter/plugin_base.dart';
@ -12,7 +13,9 @@ import '../../widgetter/types/widget_gallery.dart';
final _logger = Logger('LayoutPage');
class LayoutPage extends StatefulWidget {
const LayoutPage({super.key});
const LayoutPage({
super.key,
});
@override
State<LayoutPage> createState() => _LayoutPageState();
@ -26,7 +29,6 @@ class _LayoutPageState extends State<LayoutPage> with TickerProviderStateMixin {
bool isDragging = false;
double dragHeight = 320;
final double _minCellWidth = 150;
int _crossAxisCount = 2;
final ScrollController _scrollController = ScrollController();
bool _isLoading = false;
@ -344,7 +346,7 @@ class _LayoutPageState extends State<LayoutPage> with TickerProviderStateMixin {
children: [
Icon(
Icons.widgets_outlined,
size: 32,
size: 22,
color: Theme.of(context)
.colorScheme
.onSurface
@ -515,10 +517,11 @@ class _LayoutPageState extends State<LayoutPage> with TickerProviderStateMixin {
child: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _crossAxisCount,
crossAxisCount:
StremioCardSize.getSize(context).columns,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 1.5,
childAspectRatio: 1,
),
itemCount: widgets.length,
itemBuilder: (context, index) {

View file

@ -132,6 +132,8 @@ class StremioManifestCatalog {
String type;
String id;
String? name;
@JsonKey(name: "itemCount", defaultValue: 50)
final int itemCount;
final List<StremioManifestCatalogExtra>? extra;
@JsonKey(name: "extraRequired")
final List<String>? extraRequired_;
@ -181,6 +183,7 @@ class StremioManifestCatalog {
this.name,
this.extraRequired_,
this.extraSupported_,
this.itemCount = 50,
});
factory StremioManifestCatalog.fromRecord(RecordModel record) =>

View file

@ -285,8 +285,6 @@ class StremioAddonService {
) async {
String url = "${_getAddonBaseURL(manifest.manifestUrl!)}/catalog/$type/$id";
const perPage = 50;
final catalog = manifest.catalogs?.firstWhereOrNull((item) {
return item.type == type && item.id == id;
});
@ -341,7 +339,25 @@ class StremioAddonService {
return [];
}
final isSearch = items.firstWhereOrNull((item) {
return item.title == "search";
}) !=
null;
const perPage = 50;
if (manifest.manifestVersion == "v2") {
if (page != null &&
catalog.extraSupported?.contains("skip") == true &&
!isSearch) {
items.add(
ConnectionFilterItem(
title: "skip",
value: page * catalog.itemCount,
),
);
}
if (items.isNotEmpty) {
String filterPath = items
.map((filter) {
@ -361,11 +377,6 @@ class StremioAddonService {
}
}
final isSearch = items.firstWhereOrNull((item) {
return item.title == "search";
}) !=
null;
if (page != null &&
catalog.extraSupported?.contains("skip") == true &&
!isSearch) {

View file

@ -23,7 +23,7 @@ class _AddAddonSheetState extends State<AddAddonSheet> {
bool _isInstalling = false;
final _exampleAddons = {
"Cinemeta": "https://v3-cinemeta.strem.io/manifest.json",
"Madari Catalog": "https://catalog.madari.media/manifest.json",
"Watchhub": "https://watchhub.strem.io/manifest.json",
"Subtitles": "https://opensubtitles-v3.strem.io/manifest.json",
};

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../provider/theme_provider.dart';
@ -8,6 +9,7 @@ class AppTheme {
static final AppTheme _instance = AppTheme._internal();
static const String _primaryColorKey = 'primary_color';
static const String _isDarkModeKey = 'is_dark_mode';
final _logger = Logger('AppTheme');
factory AppTheme() {
return _instance;
@ -37,6 +39,7 @@ class AppTheme {
_themeProvider.toggleTheme();
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_isDarkModeKey, _themeProvider.isDarkMode);
_logger.info("isDarkMode ${_themeProvider.isDarkMode}");
}
Future<void> setPrimaryColor(Color color) async {

View file

@ -1,5 +1,6 @@
import 'package:cached_query/cached_query.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import 'package:madari_client/features/settings/service/selected_profile.dart';
import 'package:madari_client/features/streamio_addons/extension/query_extension.dart';
@ -117,6 +118,63 @@ class LayoutManagerState extends State<LayoutManager> {
return const CatalogFeaturedShimmer();
}
if (_layouts.isEmpty) {
return Center(
child: Container(
padding: const EdgeInsets.all(24.0),
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.design_services,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 24),
Text(
"Configure Home Layout",
style:
Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
"You need to define your home before start",
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 32),
FilledButton.icon(
onPressed: () {
context.push("/layout");
},
icon: const Icon(Icons.edit_outlined),
label: const Text(
"Configure Layout",
style: TextStyle(
fontSize: 15,
),
),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 16,
),
),
),
],
),
),
);
}
return Column(
children: [
if (widget.hasSearch)

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math';
import 'package:cached_query_flutter/cached_query_flutter.dart';
import 'package:flex_color_picker/flex_color_picker.dart';
@ -113,7 +114,7 @@ class _CatalogGridState extends State<CatalogGrid> implements Refreshable {
items,
);
return result;
return result.sublist(0, min(pageSize, result.length));
} catch (e, stack) {
_logger.severe('Error fetching catalog: $e', e, stack);
throw Exception('Failed to fetch catalog');
@ -328,7 +329,9 @@ class _CatalogGridState extends State<CatalogGrid> implements Refreshable {
builder: (context) => CatalogFullView(
title: title,
initialItems: allItems,
query: query,
queryBuilder: () {
return query;
},
prefix: widget.pluginContext.hasSearch.toString() +
widget.pluginContext.index.toString() +
widget.config["description"] +
@ -379,7 +382,8 @@ class _CatalogGridState extends State<CatalogGrid> implements Refreshable {
required String title,
}) {
final allItems = state.data?.expand((page) => page).toList() ?? [];
final itemCount = allItems.take(15).length + (allItems.isNotEmpty ? 1 : 0);
final itemCount = allItems.take(15).length +
(!(allItems.isNotEmpty && widget.pluginContext.hasSearch) ? 1 : 0);
if (allItems.isEmpty) {
return const SizedBox(
@ -464,7 +468,9 @@ class _CatalogGridState extends State<CatalogGrid> implements Refreshable {
MaterialPageRoute(
builder: (context) => CatalogFullView(
title: title,
query: getQuery(),
queryBuilder: () {
return getQuery();
},
initialItems: [],
prefix: widget.pluginContext.hasSearch.toString() +
widget.pluginContext.index.toString() +

View file

@ -1,24 +1,29 @@
import 'package:cached_query_flutter/cached_query_flutter.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import 'package:madari_client/features/widgetter/plugins/stremio/widgets/stremio_card.dart';
import 'package:shimmer/shimmer.dart';
import '../../../../streamio_addons/models/stremio_base_types.dart';
import '../utils/size.dart';
typedef GetQuery = InfiniteQuery<List<Meta>, int> Function();
class CatalogFullView extends StatefulWidget {
final List<Meta> initialItems;
final InfiniteQuery<List<Meta>, int> query;
final String prefix;
final String? title;
final GetQuery queryBuilder;
final bool supportsLoadMore;
const CatalogFullView({
super.key,
required this.initialItems,
required this.query,
required this.prefix,
required this.queryBuilder,
this.title,
this.supportsLoadMore = false,
});
@override
@ -27,20 +32,32 @@ class CatalogFullView extends StatefulWidget {
class _CatalogFullViewState extends State<CatalogFullView> {
final ScrollController _scrollController = ScrollController();
final _logger = Logger("CatalogFullView");
late final InfiniteQuery<List<Meta>, int> _query = widget.query;
late final InfiniteQuery<List<Meta>, int> _query;
@override
void initState() {
super.initState();
_query = widget.queryBuilder();
}
Future<void> _loadMoreData() async {
final currentState = _query.state;
if (currentState.lastPage != null && currentState.lastPage!.isEmpty) {
_logger.info("Last page is empty");
return;
}
if (currentState.status == QueryStatus.loading) {
_logger.info("Status is loading");
return;
}
_logger.info("Loading next page");
await _query.getNextPage();
}