diff --git a/example/piped_client_example.dart b/example/piped_client_example.dart index fa6266b..1136df8 100644 --- a/example/piped_client_example.dart +++ b/example/piped_client_example.dart @@ -1,24 +1,194 @@ import 'package:piped_client/piped_client.dart'; -void main() async { - final client = PipedClient(); +import 'package:flutter/material.dart'; - final instances = await client.instanceList(); - for (final instance in instances) { - print( - 'Instance: ${instance.name}${instance.locations} => ${instance.apiUrl}'); + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatefulWidget { + @override + MyAppState createState() => MyAppState(); +} + +class MyAppState extends State { + // 使用 PipedClient 的默认构造函数,初始时使用默认值 + final _pipedClient = PipedClient(); + final _instanceUrlController = TextEditingController(); + final _instanceListUrlController = TextEditingController(); + List _instances = []; + String _searchQuery = ""; + List _searchResults = []; + + @override + void initState() { + super.initState(); + _fetchInstances(); // 初始获取实例列表 } - final result = await client.search('piped'); + Future _fetchInstances() async { + try { + final instances = await _pipedClient.instanceList(); + setState(() { + _instances = instances; + }); + _printInstances(); + } catch (e) { + print("Error fetching instances: $e"); + _showError("Failed to fetch instances: $e"); + } + } - for (final item in result.items) { - if (item is PipedSearchItemStream) { - print('Stream: ${item.title} => ${item.url}'); - } else if (item is PipedSearchItemChannel) { - print('Channel: ${item.name} => ${item.url}'); - } else if (item is PipedSearchItemPlaylist) { - print('Playlist: ${item.name} => ${item.url}'); + Future _search() async { + if (_searchQuery.isEmpty) return; + try { + final result = await _pipedClient.search(_searchQuery); + setState(() { + _searchResults = result.items; + }); + _printSearchResults(); + } catch (e) { + print("Error searching: $e"); + _showError("Failed to search: $e"); } } -} + + void _showError(String message) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); + } + + void _printInstances() { + print("Available Instances:"); + for (final instance in _instances) { + print('Instance: ${instance.name} ${instance.locations.join(", ")} => ${instance.apiUrl}'); + } + } + + void _printSearchResults() { + print("\nSearch results for '$_searchQuery':"); + for (final item in _searchResults) { + if (item is PipedSearchItemStream) { + print('Stream: ${item.title} (${item.uploaderName}) - ${item.url}'); + _fetchStreamDetails(item.url); + } else if (item is PipedSearchItemChannel) { + print('Channel: ${item.name} - ${item.url}'); + } else if (item is PipedSearchItemPlaylist) { + print('Playlist: ${item.name} - ${item.url}'); + } + } + } + + Future _fetchStreamDetails(String url) async { + try { + final videoId = Uri.parse(url).pathSegments.last; + final streamResponse = await _pipedClient.streams(videoId); + print(' └─ Audio Streams: ${streamResponse.audioStreams.length}'); + print(' └─ Video Streams: ${streamResponse.videoStreams.length}'); + } catch (e) { + print(' └─ Error fetching stream details: $e'); + } + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Piped Client UI Example'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _instanceUrlController, + decoration: const InputDecoration( + labelText: 'Instance URL', + hintText: 'Enter custom instance URL (optional)', + ), + ), + TextField( + controller: _instanceListUrlController, + decoration: const InputDecoration( + labelText: 'Instance List URL', + hintText: 'Enter custom instance list URL (optional)', + ), + ), + ElevatedButton( + onPressed: () { + // 更新 PipedClient 的设置 + _pipedClient.setInstanceUrl(_instanceUrlController.text); + _pipedClient.setCustomInstanceListUrl(_instanceListUrlController.text); + _fetchInstances(); // 重新获取 Instance 列表 + }, + child: const Text('Update Settings and Fetch Instances'), + ), + const SizedBox(height: 20), + const Text("Available Instances:", style: TextStyle(fontWeight: FontWeight.bold)), + Expanded( + child: ListView.builder( + itemCount: _instances.length, + itemBuilder: (context, index) { + final instance = _instances[index]; + return ListTile( + title: Text(instance.name), + subtitle: Text("${instance.apiUrl} - ${instance.locations.join(', ')}"), + ); + }, + ), + ), + TextField( + onChanged: (value) { + _searchQuery = value; + }, + decoration: const InputDecoration( + labelText: 'Search', + hintText: 'Enter search query', + ), + ), + ElevatedButton( + onPressed: _search, + child: const Text('Search'), + ), + Expanded( + child: ListView.builder( + itemCount: _searchResults.length, + itemBuilder: (context, index) { + final item = _searchResults[index]; + if (item is PipedSearchItemStream) { + return ListTile( + title: Text(item.title), + subtitle: Text('Stream - ${item.uploaderName}'), + ); + } else if (item is PipedSearchItemChannel) { + return ListTile( + title: Text(item.name), + subtitle: Text('Channel'), + ); + } else if (item is PipedSearchItemPlaylist) { + return ListTile( + title: Text(item.name), + subtitle: Text('Playlist'), + ); + } + return const SizedBox.shrink(); + }, + ), + ), + ], + ), + ), + ), + ); + } + + @override + void dispose() { + _instanceUrlController.dispose(); + _instanceListUrlController.dispose(); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/src/client.dart b/lib/src/client.dart index f7b9f49..0ecec61 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -20,13 +20,25 @@ enum PipedFilter { class PipedClient { final Dio client; final bool debug; + String? _customInstanceListUrl; // Used to store the user-defined Instance list URL + String _instance; // Used to store the user-defined Piped instance URL + + /// Default Instance list URL + static const String defaultInstanceListUrl = + "https://raw.githubusercontent.com/wiki/TeamPiped/Piped-Frontend/Instances.md"; + + /// Default Piped instance URL + static const String defaultInstance = "https://pipedapi.kavin.rocks"; PipedClient({ - String instance = "https://pipedapi.kavin.rocks", + String? instance, // Optional: User-defined Piped instance URL this.debug = false, - }) : client = Dio( + String? customInstanceListUrl, // Optional: User-defined Instance list URL + }) : _instance = instance ?? defaultInstance, + _customInstanceListUrl = customInstanceListUrl, + client = Dio( BaseOptions( - baseUrl: instance, + baseUrl: instance ?? defaultInstance, // Use custom instance or default instance responseType: ResponseType.json, ), ); @@ -51,8 +63,11 @@ class PipedClient { } Future> instanceList() async { + // Use user-defined link, if not then use the default link + final String url = _customInstanceListUrl ?? defaultInstanceListUrl; + final res = await client.get( - "https://raw.githubusercontent.com/wiki/TeamPiped/Piped-Frontend/Instances.md", + url, options: Options( responseType: ResponseType.plain, ), @@ -86,4 +101,25 @@ class PipedClient { .where((instance) => instance.apiUrl.startsWith("http")) .toList(); } -} + + /// Set a custom Instance list URL + void setCustomInstanceListUrl(String url) { + _customInstanceListUrl = url; + } + + /// Set a custom Piped instance URL + void setInstanceUrl(String url) { + _instance = url; + client.options.baseUrl = url; + } + + /// Get the current Piped instance URL + String getInstanceUrl() { + return _instance; + } + + /// Get the current Instance list URL + String getInstanceListUrl() { + return _customInstanceListUrl ?? defaultInstanceListUrl; + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 5ba4f3e..d2a1604 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,8 @@ environment: dependencies: dio: ^5.1.2 + flutter: + sdk: flutter json_annotation: ^4.8.1 dev_dependencies: