mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-04-20 18:22:04 +00:00
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:
parent
0d7573ee22
commit
a38b3d1235
12 changed files with 184 additions and 102 deletions
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() +
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue