mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-03-11 17:15:39 +00:00
Project import generated by Copybara.
GitOrigin-RevId: 7a1d4421cba8507811cf2b329e63a7065546b86f
This commit is contained in:
parent
6e360022ca
commit
2a0e255f62
10 changed files with 530 additions and 245 deletions
6
Makefile
6
Makefile
|
|
@ -10,10 +10,10 @@ build_web:
|
|||
flutter build web --target lib/main_web.dart --release --pwa-strategy none --wasm
|
||||
|
||||
build_mac:
|
||||
flutter build macos --target lib/main.dart --release --no-tree-shake-icons
|
||||
flutter build macos --target lib/main.dart --release
|
||||
|
||||
build_android:
|
||||
flutter build apk --release --no-tree-shake-icons
|
||||
flutter build apk --release
|
||||
|
||||
build_windows:
|
||||
flutter build windows --release --no-tree-shake-icons
|
||||
flutter build windows --release
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ abstract class BaseConnectionService {
|
|||
Connection item,
|
||||
ConnectionTypeRecord type,
|
||||
) {
|
||||
print(type);
|
||||
switch (type.type) {
|
||||
case "stremio_addons":
|
||||
return StremioConnectionService(
|
||||
|
|
@ -91,8 +90,12 @@ abstract class BaseConnectionService {
|
|||
|
||||
Future<LibraryItem?> getItemById(LibraryItem id);
|
||||
|
||||
Stream<List<StreamList>> getStreams(LibraryRecord library, LibraryItem id,
|
||||
{String? season, String? episode});
|
||||
Stream<List<StreamList>> getStreams(
|
||||
LibraryRecord library,
|
||||
LibraryItem id, {
|
||||
String? season,
|
||||
String? episode,
|
||||
});
|
||||
|
||||
BaseConnectionService({
|
||||
required this.connectionId,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import './base_connection_service.dart';
|
|||
|
||||
part 'stremio_connection_service.g.dart';
|
||||
|
||||
final Map<String, String> manifestCache = {};
|
||||
|
||||
class StremioConnectionService extends BaseConnectionService {
|
||||
final StremioConfig config;
|
||||
|
||||
|
|
@ -155,8 +157,15 @@ class StremioConnectionService extends BaseConnectionService {
|
|||
}
|
||||
|
||||
Future<StremioManifest> _getManifest(String url) async {
|
||||
final result = await http.get(Uri.parse(url));
|
||||
final body = jsonDecode(result.body);
|
||||
final String result;
|
||||
if (manifestCache.containsKey(url)) {
|
||||
result = manifestCache[url]!;
|
||||
} else {
|
||||
result = (await http.get(Uri.parse(url))).body;
|
||||
manifestCache[url] = result;
|
||||
}
|
||||
|
||||
final body = jsonDecode(result);
|
||||
final resultFinal = StremioManifest.fromJson(body);
|
||||
return resultFinal;
|
||||
}
|
||||
|
|
@ -169,7 +178,49 @@ class StremioConnectionService extends BaseConnectionService {
|
|||
|
||||
@override
|
||||
Future<List<ConnectionFilter<T>>> getFilters<T>(LibraryRecord library) async {
|
||||
return [];
|
||||
final configItems = getConfig(library.config);
|
||||
List<ConnectionFilter<T>> filters = [];
|
||||
|
||||
try {
|
||||
for (final addon in configItems) {
|
||||
final addonManifest = await _getManifest(addon.addon);
|
||||
|
||||
if ((addonManifest.catalogs?.isEmpty ?? true) == true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final catalogs = addonManifest.catalogs!.where((item) {
|
||||
return item.id == addon.item.id && item.type == addon.item.type;
|
||||
}).toList();
|
||||
|
||||
for (final catalog in catalogs) {
|
||||
if (catalog.extra == null) {
|
||||
continue;
|
||||
}
|
||||
for (final extraItem in catalog.extra!) {
|
||||
if (extraItem.options == null ||
|
||||
extraItem.options?.isEmpty == true) {
|
||||
filters.add(
|
||||
ConnectionFilter<T>(
|
||||
title: extraItem.name,
|
||||
type: ConnectionFilterType.text,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
filters.add(
|
||||
ConnectionFilter<T>(
|
||||
title: extraItem.name,
|
||||
type: ConnectionFilterType.options,
|
||||
values: extraItem.options?.whereType<T>().toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -105,10 +105,12 @@ class StremioManifestCatalog {
|
|||
String type;
|
||||
String id;
|
||||
String? name;
|
||||
final List<StremioManifestCatalogExtra>? extra;
|
||||
|
||||
StremioManifestCatalog({
|
||||
required this.id,
|
||||
required this.type,
|
||||
this.extra,
|
||||
this.name,
|
||||
});
|
||||
|
||||
|
|
@ -121,6 +123,30 @@ class StremioManifestCatalog {
|
|||
Map<String, dynamic> toJson() => _$StremioManifestCatalogToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class StremioManifestCatalogExtra {
|
||||
final String name;
|
||||
final List<dynamic>? options;
|
||||
|
||||
StremioManifestCatalogExtra({
|
||||
required this.name,
|
||||
required this.options,
|
||||
});
|
||||
|
||||
factory StremioManifestCatalogExtra.fromJson(Map<String, dynamic> json) {
|
||||
try {
|
||||
return _$StremioManifestCatalogExtraFromJson(json);
|
||||
} catch (e) {
|
||||
return StremioManifestCatalogExtra(
|
||||
name: "Unable to parse",
|
||||
options: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$StremioManifestCatalogExtraToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class StremioConfig {
|
||||
List<String> addons;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:pocketbase/pocketbase.dart';
|
|||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
import '../../../../utils/grid.dart';
|
||||
import '../stremio/stremio_filter.dart';
|
||||
|
||||
final pb = AppEngine.engine.pb;
|
||||
|
||||
|
|
@ -41,7 +42,9 @@ class _RenderLibraryListState extends State<RenderLibraryList> {
|
|||
query: query,
|
||||
builder: (ctx, state) {
|
||||
if (state.status == QueryStatus.loading) {
|
||||
return const SpinnerCards();
|
||||
return const Center(
|
||||
child: SpinnerCards(),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status == QueryStatus.error) {
|
||||
|
|
@ -140,6 +143,8 @@ class __RenderLibraryListState extends State<_RenderLibraryList> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
|
||||
loadFilters();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -150,22 +155,22 @@ class __RenderLibraryListState extends State<_RenderLibraryList> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
List<ConnectionFilterItem> filters = [];
|
||||
|
||||
InfiniteQuery getQuery() {
|
||||
return InfiniteQuery<List<LibraryItem>, int>(
|
||||
key:
|
||||
"loadLibrary${widget.item.id}${widget.filters.map((res) => "${res.title}=${res.value}").join("&")}",
|
||||
"loadLibrary${widget.item.id}${(widget.filters + filters).map((res) => "${res.title}=${res.value}").join("&")}",
|
||||
queryFn: (page) {
|
||||
return service
|
||||
.getItems(
|
||||
widget.item,
|
||||
items: widget.filters,
|
||||
items: widget.filters + filters,
|
||||
page: page,
|
||||
)
|
||||
.then((docs) {
|
||||
return docs.items.toList();
|
||||
}).catchError((e, stack) {
|
||||
print(e);
|
||||
print(stack);
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
|
@ -180,8 +185,94 @@ class __RenderLibraryListState extends State<_RenderLibraryList> {
|
|||
|
||||
bool isUnsupported = false;
|
||||
|
||||
loadFilters() async {
|
||||
final filters = await service.getFilters(widget.item);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
filterList = filters;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List<ConnectionFilter>? filterList;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.isGrid) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.item.title),
|
||||
),
|
||||
body: SizedBox(
|
||||
height: MediaQuery.of(context).size.height - 96,
|
||||
child: Flex(
|
||||
direction: Axis.vertical,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
if (filterList == null)
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 36,
|
||||
width: 120,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10.0,
|
||||
right: 10.0,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const SizedBox(
|
||||
height: 36,
|
||||
width: 120,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (filterList != null)
|
||||
InlineFilters(
|
||||
filters: filterList ?? [],
|
||||
filterCallback: (item) {
|
||||
filters = item;
|
||||
|
||||
setState(() {
|
||||
query = getQuery();
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height - 96,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10.0,
|
||||
right: 10.0,
|
||||
),
|
||||
child: _buildBody(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _buildBody();
|
||||
}
|
||||
|
||||
_buildBody() {
|
||||
final itemWidth = _getItemWidth(context);
|
||||
final listHeight = _getListHeight(context);
|
||||
|
||||
|
|
@ -320,6 +411,7 @@ class SpinnerCards extends StatelessWidget {
|
|||
height: itemHeight,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, _) {
|
||||
return SizedBox(
|
||||
width: itemWidth,
|
||||
|
|
|
|||
130
lib/features/connections/widget/stremio/stremio_filter.dart
Normal file
130
lib/features/connections/widget/stremio/stremio_filter.dart
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:madari_client/features/connection/services/stremio_service.dart';
|
||||
|
||||
import '../../service/base_connection_service.dart';
|
||||
|
||||
typedef FilterCallback = void Function(List<ConnectionFilterItem> item);
|
||||
|
||||
class InlineFilters extends StatefulWidget {
|
||||
final List<ConnectionFilter<dynamic>> filters;
|
||||
final FilterCallback filterCallback;
|
||||
|
||||
const InlineFilters({
|
||||
super.key,
|
||||
required this.filters,
|
||||
required this.filterCallback,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InlineFilters> createState() => _InlineFiltersState();
|
||||
}
|
||||
|
||||
class _InlineFiltersState extends State<InlineFilters> {
|
||||
final Map<String, dynamic> _selectedValues = {};
|
||||
|
||||
List<ConnectionFilterItem> generateFilterItem() {
|
||||
final List<ConnectionFilterItem> items = [];
|
||||
|
||||
for (final item in _selectedValues.keys) {
|
||||
items.add(
|
||||
ConnectionFilterItem(title: item, value: _selectedValues[item]!),
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
onChange() {
|
||||
widget.filterCallback(generateFilterItem());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: widget.filters
|
||||
.where((filter) => filter.type == ConnectionFilterType.options)
|
||||
.map((filter) {
|
||||
final isSelected = _selectedValues.containsKey(filter.title);
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: InputChip(
|
||||
label: Text(
|
||||
(isSelected ? _selectedValues[filter.title] : filter.title)
|
||||
.toString()
|
||||
.capitalize(),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: theme.textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
onPressed: () {
|
||||
if (isSelected) {
|
||||
setState(() {
|
||||
_selectedValues.remove(filter.title);
|
||||
});
|
||||
|
||||
onChange();
|
||||
} else {
|
||||
_showOptionsDialog(filter);
|
||||
}
|
||||
},
|
||||
deleteIcon: isSelected
|
||||
? const Icon(
|
||||
Icons.close,
|
||||
)
|
||||
: null,
|
||||
onDeleted: isSelected
|
||||
? () {
|
||||
setState(() {
|
||||
_selectedValues.remove(filter.title);
|
||||
|
||||
onChange();
|
||||
});
|
||||
}
|
||||
: null,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showOptionsDialog(ConnectionFilter<dynamic> filter) async {
|
||||
final selectedValue = await showDialog<dynamic>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: Text(filter.title),
|
||||
children: (filter.values ?? []).map((value) {
|
||||
return SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, value);
|
||||
},
|
||||
child: Text(value.toString()),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (selectedValue != null) {
|
||||
setState(() {
|
||||
_selectedValues[filter.title] = selectedValue;
|
||||
});
|
||||
|
||||
onChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -73,12 +73,14 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _validateAddonUrl(String url) async {
|
||||
Future<void> _validateAddonUrl(String url_) async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
final url = url_.replaceFirst("stremio://", "https://");
|
||||
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse(
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ class ZeeeWatchHistory extends BaseWatchHistory {
|
|||
|
||||
ZeeeWatchHistory() {
|
||||
_listener = AppEngine.engine.pb.authStore.onChange.listen((auth) {
|
||||
if (!AppEngine.engine.pb.authStore.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
_initializeFromServer().then((docs) {
|
||||
if (_syncTimer != null) {
|
||||
_syncTimer!.cancel();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:cached_query_flutter/cached_query_flutter.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:madari_client/engine/library.dart';
|
||||
import 'package:madari_client/features/connections/service/base_connection_service.dart';
|
||||
|
||||
import '../features/connections/widget/base/render_library_list.dart';
|
||||
|
|
@ -10,6 +11,7 @@ import '../features/getting_started/container/getting_started.dart';
|
|||
class HomeTabPage extends StatefulWidget {
|
||||
final String? search;
|
||||
final bool hideAppBar;
|
||||
final LibraryRecordResponse? defaultLibraries;
|
||||
|
||||
static String get routeName => "/";
|
||||
|
||||
|
|
@ -17,6 +19,7 @@ class HomeTabPage extends StatefulWidget {
|
|||
super.key,
|
||||
this.search,
|
||||
this.hideAppBar = kIsWeb,
|
||||
this.defaultLibraries,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -24,10 +27,18 @@ class HomeTabPage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _HomeTabPageState extends State<HomeTabPage> {
|
||||
final query = Query(
|
||||
queryFn: () => BaseConnectionService.getLibraries(),
|
||||
late final query = Query(
|
||||
queryFn: () {
|
||||
if (widget.defaultLibraries != null) {
|
||||
return Future.value(
|
||||
widget.defaultLibraries,
|
||||
);
|
||||
}
|
||||
|
||||
return BaseConnectionService.getLibraries();
|
||||
},
|
||||
key: [
|
||||
"home",
|
||||
"home${widget.defaultLibraries?.data.length ?? 0}${widget.search ?? ""}",
|
||||
],
|
||||
);
|
||||
|
||||
|
|
@ -54,128 +65,111 @@ class _HomeTabPageState extends State<HomeTabPage> {
|
|||
style: GoogleFonts.montserrat(),
|
||||
),
|
||||
),
|
||||
body: QueryBuilder(
|
||||
query: query,
|
||||
builder: (context, state) {
|
||||
if (QueryStatus.error == state.status) {
|
||||
return _buildError(state.error);
|
||||
}
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await query.refetch();
|
||||
return;
|
||||
},
|
||||
child: QueryBuilder(
|
||||
query: query,
|
||||
builder: (context, state) {
|
||||
if (QueryStatus.error == state.status) {
|
||||
return _buildError(state.error);
|
||||
}
|
||||
|
||||
final data = state.data;
|
||||
final data = state.data;
|
||||
|
||||
if (data == null) {
|
||||
return const Text("Loading");
|
||||
}
|
||||
if (data == null) {
|
||||
return const Text("Loading");
|
||||
}
|
||||
|
||||
if (data.data.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 24,
|
||||
left: 12,
|
||||
right: 12,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: GettingStartedScreen(
|
||||
onCallback: () {
|
||||
query.refetch();
|
||||
},
|
||||
if (data.data.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 24,
|
||||
left: 12,
|
||||
right: 12,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: GettingStartedScreen(
|
||||
onCallback: () {
|
||||
query.refetch();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
child: ListView.builder(
|
||||
itemBuilder: (item, index) {
|
||||
final item = data.data[index];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: ListView.builder(
|
||||
itemBuilder: (item, index) {
|
||||
final item = data.data[index];
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
item.title,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(item.title),
|
||||
),
|
||||
body: SizedBox(
|
||||
height: MediaQuery.of(context)
|
||||
.size
|
||||
.height -
|
||||
96,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: RenderLibraryList(
|
||||
item: item,
|
||||
isGrid: true,
|
||||
filters: [
|
||||
if ((widget.search ?? "")
|
||||
.trim() !=
|
||||
"")
|
||||
ConnectionFilterItem(
|
||||
title: "search",
|
||||
value: widget.search,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
item.title,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ShowMorePage(
|
||||
item: item,
|
||||
search: widget.search,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"Show more",
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: Colors.white70,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"Show more",
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
RenderLibraryList(
|
||||
item: item,
|
||||
filters: [
|
||||
if ((widget.search ?? "").trim() != "")
|
||||
ConnectionFilterItem(
|
||||
title: "search",
|
||||
value: widget.search,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: data.data.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
RenderLibraryList(
|
||||
item: item,
|
||||
filters: [
|
||||
if ((widget.search ?? "").trim() != "")
|
||||
ConnectionFilterItem(
|
||||
title: "search",
|
||||
value: widget.search,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: data.data.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -220,3 +214,29 @@ class _HomeTabPageState extends State<HomeTabPage> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShowMorePage extends StatelessWidget {
|
||||
final LibraryRecord item;
|
||||
final String? search;
|
||||
|
||||
const ShowMorePage({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.search,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RenderLibraryList(
|
||||
item: item,
|
||||
isGrid: true,
|
||||
filters: [
|
||||
if ((search ?? "").trim() != "")
|
||||
ConnectionFilterItem(
|
||||
title: "search",
|
||||
value: search,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../engine/engine.dart';
|
||||
import '../features/connections/service/base_connection_service.dart';
|
||||
import '../features/connections/types/base/base.dart';
|
||||
import 'home_tab.page.dart';
|
||||
|
||||
class SearchPage extends StatefulWidget {
|
||||
|
|
@ -15,15 +18,63 @@ class SearchPage extends StatefulWidget {
|
|||
|
||||
class _SearchPageState extends State<SearchPage> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String _selectedFilter = 'All';
|
||||
Timer? _debounceTimer;
|
||||
bool _isSearchFocused = false;
|
||||
final List<String> _filterOptions = ['All', 'Videos', 'PDFs', 'Images'];
|
||||
String _debouncedSearchTerm = '';
|
||||
LibraryRecordResponse? searchLibrariesList;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
loadLibrariesWhichSupportSearch();
|
||||
}
|
||||
|
||||
loadLibrariesWhichSupportSearch() async {
|
||||
final library =
|
||||
await AppEngine.engine.pb.collection("library").getFullList();
|
||||
|
||||
final record = library
|
||||
.map(
|
||||
(item) => LibraryRecord.fromRecord(item),
|
||||
)
|
||||
.where((item) {
|
||||
return item.connectionType == "stremio_addons";
|
||||
}).toList();
|
||||
|
||||
final List<LibraryRecord> records = [];
|
||||
|
||||
for (final item in record) {
|
||||
final result =
|
||||
await BaseConnectionService.connectionByIdRaw(item.connection);
|
||||
|
||||
final service = BaseConnectionService.connectionById(result);
|
||||
|
||||
final filters = await service.getFilters(item);
|
||||
|
||||
final hasFilter = filters.where((item) {
|
||||
return item.title == "search";
|
||||
}).isNotEmpty;
|
||||
|
||||
if (hasFilter) {
|
||||
records.add(item);
|
||||
if (mounted) {
|
||||
searchLibrariesList = LibraryRecordResponse(
|
||||
data: records,
|
||||
);
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchLibrariesList = LibraryRecordResponse(
|
||||
data: records,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -46,7 +97,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size(double.infinity, 114),
|
||||
preferredSize: const Size(double.infinity, 76),
|
||||
child: Container(
|
||||
color: Colors.grey[900],
|
||||
padding: const EdgeInsets.symmetric(
|
||||
|
|
@ -96,47 +147,32 @@ class _SearchPageState extends State<SearchPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _filterOptions.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(width: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final filter = _filterOptions[index];
|
||||
final isSelected = _selectedFilter == filter;
|
||||
return FilterChip(
|
||||
label: Text(
|
||||
filter,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
selected: isSelected,
|
||||
showCheckmark: false,
|
||||
onSelected: (bool selected) {
|
||||
setState(() => _selectedFilter = filter);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: _debouncedSearchTerm.isEmpty
|
||||
? Center(
|
||||
child: _buildEmptyState(),
|
||||
)
|
||||
: HomeTabPage(
|
||||
hideAppBar: true,
|
||||
search: _debouncedSearchTerm,
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
child: _buildBody(),
|
||||
onRefresh: () {
|
||||
return loadLibrariesWhichSupportSearch();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return _debouncedSearchTerm.isEmpty
|
||||
? Center(
|
||||
child: _buildEmptyState(),
|
||||
)
|
||||
: HomeTabPage(
|
||||
hideAppBar: true,
|
||||
search: _debouncedSearchTerm,
|
||||
defaultLibraries: searchLibrariesList,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
|
|
@ -160,83 +196,4 @@ class _SearchPageState extends State<SearchPage> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchResults() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).size.width > 1200
|
||||
? 5
|
||||
: MediaQuery.of(context).size.width > 800
|
||||
? 4
|
||||
: MediaQuery.of(context).size.width > 600
|
||||
? 3
|
||||
: 2,
|
||||
childAspectRatio: 16 / 9,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
mainAxisExtent: 200,
|
||||
),
|
||||
itemBuilder: (context, index) => _buildResultCard(index),
|
||||
itemCount: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResultCard(int index) {
|
||||
return InkWell(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[900],
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_selectedFilter == 'PDFs'
|
||||
? Icons.picture_as_pdf
|
||||
: _selectedFilter == 'Videos'
|
||||
? Icons.play_circle_filled
|
||||
: Icons.image,
|
||||
size: 40,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Title ${index + 1}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'2024 • Category',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[400],
|
||||
fontSize: 12,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue