Skip to content

Commit

Permalink
add all the channel data
Browse files Browse the repository at this point in the history
fix youtube playlists
revamp search to use the native components.
  • Loading branch information
lamarios committed Feb 18, 2023
1 parent eb1e146 commit e11d1ae
Show file tree
Hide file tree
Showing 14 changed files with 335 additions and 236 deletions.
71 changes: 69 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import 'dart:async';

import 'package:dynamic_color/dynamic_color.dart';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:fbroadcast/fbroadcast.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:invidious/globals.dart';
import 'package:invidious/models/searchResult.dart';
import 'package:invidious/views/playlists.dart';
import 'package:invidious/views/popular.dart';
import 'package:invidious/views/search.dart';
Expand Down Expand Up @@ -95,7 +97,7 @@ class MyApp extends StatelessWidget {
colorScheme: lightColorScheme,
),
darkTheme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme),
home: showWizard? const WelcomeWizard() : const Home());
home: showWizard ? const WelcomeWizard() : const Home());
});
}
}
Expand Down Expand Up @@ -209,7 +211,8 @@ class HomeState extends State<Home> {
actions: [
GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => const Search()));
// Navigator.push(context, MaterialPageRoute(builder: (context) => const Search()));
showSearch(context: context, delegate: MySearchDelegate());
},
child: Padding(
padding: const EdgeInsets.all(8.0),
Expand Down Expand Up @@ -279,6 +282,7 @@ class HomeState extends State<Home> {
),
const Playlists(
key: ValueKey(3),
canDeleteVideos: true,
)
][selectedIndex],
transitionBuilder: (Widget child, Animation<double> animation) {
Expand All @@ -288,3 +292,66 @@ class HomeState extends State<Home> {
])));
}
}

class MySearchDelegate extends SearchDelegate {
@override
List<Widget>? buildActions(BuildContext context) => [
IconButton(
onPressed: () {
if (query.isEmpty) {
close(context, null);
} else {
query = '';
}
},
icon: const Icon(Icons.clear))
];

@override
Widget? buildLeading(BuildContext context) => IconButton(onPressed: () => close(context, null), icon: Icon(Icons.arrow_back));

@override
Widget buildResults(BuildContext context) {
return FutureBuilder<SearchResults>(
future: service.search(query),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done && snapshot.data != null) {
return Search(results: snapshot.data!);
} else if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
} else {
return const SizedBox.shrink();
}
},
);
}

@override
Widget buildSuggestions(BuildContext context) {
// service.getSearchSuggestion(query);
return FutureBuilder(
future: service.getSearchSuggestion(query),
builder: (context, snapshot) {
List<String> suggestions = [];
if (snapshot.connectionState == ConnectionState.done) {
suggestions = snapshot.data?.suggestions ?? [];
}

return ListView.builder(
itemCount: suggestions.length,
itemBuilder: (context, index) {
String sugg = suggestions[index];
return ListTile(
title: Text(suggestions[index]),
onTap: () {
query = sugg;
showResults(context);
},
);
});
},
);
}
}
2 changes: 1 addition & 1 deletion lib/models/channelVideos.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ part 'channelVideos.g.dart';
@JsonSerializable()
class ChannelVideos{
List<VideoInList> videos;
String continuation;
String? continuation;

ChannelVideos(this.videos, this.continuation);

Expand Down
2 changes: 1 addition & 1 deletion lib/models/channelVideos.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions lib/models/searchResult.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'dart:core';

import 'channel.dart';
import 'playlist.dart';
import 'videoInList.dart';

class SearchResults {
List<VideoInList> videos = [];
List<Playlist> playlists = [];
List<Channel> channels = [];
}
41 changes: 36 additions & 5 deletions lib/service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:invidious/database.dart';
import 'package:invidious/globals.dart';
import 'package:invidious/models/errors/invidiousServiceError.dart';
import 'package:invidious/models/playlist.dart';
import 'package:invidious/models/searchResult.dart';
import 'package:invidious/models/sponsorSegment.dart';
import 'package:invidious/models/userFeed.dart';
import 'package:invidious/models/video.dart';
Expand Down Expand Up @@ -36,13 +37,15 @@ const ADD_DELETE_SUBSCRIPTION = '/api/v1/auth/subscriptions/:ucid';
const GET_COMMENTS = '/api/v1/comments/:id';
const GET_CHANNEL = '/api/v1/channels/:id';
const GET_CHANNEL_VIDEOS = '/api/v1/channels/:id/videos';
const GET_CHANNEL_STREAMS = '/api/v1/channels/:id/streams';
const GET_SPONSOR_SEGMENTS = 'https://sponsor.ajay.app/api/skipSegments?videoID=:id';
const GET_USER_PLAYLISTS = '/api/v1/auth/playlists';
const POST_USER_PLAYLIST = '/api/v1/auth/playlists';
const GET_CHANNEL_PLAYLISTS = '/api/v1/channels/:id/playlists';
const POST_USER_PLAYLIST_VIDEO = '/api/v1/auth/playlists/:id/videos';
const DELETE_USER_PLAYLIST = '/api/v1/auth/playlists/:id';
const DELETE_USER_PLAYLIST_VIDEO = '/api/v1/auth/playlists/:id/videos/:index';
const GET_PUBLIC_PLAYLIST = '/api/v1/playlists/:id';

const MAX_PING = 9007199254740991;

Expand Down Expand Up @@ -128,13 +131,28 @@ class Service {
return List<VideoInList>.from(i.map((e) => VideoInList.fromJson(e)));
}

Future<List<VideoInList>> search(String query) async {
Future<SearchResults> search(String query) async {
String url = db.getCurrentlySelectedServer().url + SEARCH.replaceAll(":q", query);
log.info('Calling $url');
final response = await http.get(Uri.parse(url));
Iterable i = handleResponse(response);
// only getting videos for now
return List<VideoInList>.from(i.where((e) => e['type'] == 'video').map((e) => VideoInList.fromJson(e)));
SearchResults results = SearchResults();
i.forEach((e) {
switch(e['type']){
case 'video':
results.videos.add(VideoInList.fromJson(e));
break;
case 'playlist':
results.playlists.add(Playlist.fromJson(e));
break;
case 'Channel':
results.channels.add(Channel.fromJson(e));
}

});

return results;
}

Future<UserFeed> getUserFeed({int? maxResults, int? page}) async {
Expand Down Expand Up @@ -250,9 +268,15 @@ class Service {
}

Future<ChannelVideos> getChannelVideos(String channelId, String? continuation) async {
String url = db.getCurrentlySelectedServer().url + (GET_CHANNEL_VIDEOS.replaceAll(":id", channelId)) + (continuation != null ? '?continuation=$continuation' : '');
log.info('Calling $url');
final response = await http.get(Uri.parse(url), headers: {'Content-Type': 'application/json; charset=utf-16'});
Uri uri = buildUrl(GET_CHANNEL_VIDEOS, pathParams: {':id': channelId}, query: {'continuation': continuation});
final response = await http.get(uri, headers: {'Content-Type': 'application/json; charset=utf-16'});

return ChannelVideos.fromJson(handleResponse(response));
}

Future<ChannelVideos> getChannelStreams(String channelId, String? continuation) async {
Uri uri = buildUrl(GET_CHANNEL_STREAMS, pathParams: {':id': channelId}, query: {'continuation': continuation});
final response = await http.get(uri, headers: {'Content-Type': 'application/json; charset=utf-16'});

return ChannelVideos.fromJson(handleResponse(response));
}
Expand Down Expand Up @@ -365,4 +389,11 @@ class Service {

return servers.where((s) => (s.api ?? false) && (s.stats?.openRegistrations ?? false)).toList();
}

Future<Playlist> getPublicPlaylists(String playlistId, {int? page}) async {
Uri uri = buildUrl(GET_PUBLIC_PLAYLIST, pathParams: {':id': playlistId}, query: {'page': page?.toString()});

final response = await http.get(uri);
return Playlist.fromJson(handleResponse(response));
}
}
48 changes: 14 additions & 34 deletions lib/views/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,31 +36,20 @@ class ChannelView extends StatefulWidget {

class ChannelViewState extends State<ChannelView> with AfterLayoutMixin<ChannelView> {
bool isSubscribed = false;
ScrollController scrollController = ScrollController();
int selectedIndex = 0;
Channel? channel;
bool loading = true;
double bannerHeight = 200;
double opacity = 1;

@override
void initState() {
super.initState();
scrollController.addListener(onScroll);
}

@override
dispose() async {
scrollController.dispose();
super.dispose();
}

onScroll() {
setState(() {
bannerHeight = max(0, 200 - scrollController.offset);
opacity = 1 - min(1, ((scrollController.offset) / 200));
});
}

toggleSubscription() async {
if (this.isSubscribed) {
Expand Down Expand Up @@ -106,15 +95,15 @@ class ChannelViewState extends State<ChannelView> with AfterLayoutMixin<ChannelV
elevation: 0,
onDestinationSelected: (int index) {
setState(() {
scrollController.animateTo(0, duration: animationDuration, curve: Curves.easeInOutQuad);
selectedIndex = index;
});
},
selectedIndex: selectedIndex,
destinations: const <Widget>[
NavigationDestination(icon: Icon(Icons.info), label: 'Info'),
NavigationDestination(icon: Icon(Icons.play_arrow), label: 'Videos'),
// NavigationDestination(icon: Icon(Icons.playlist_play), label: 'Playlists')
NavigationDestination(icon: Icon(Icons.stream), label: 'Streams'),
NavigationDestination(icon: Icon(Icons.playlist_play), label: 'Playlists')
],
),
body: SafeArea(
Expand All @@ -128,16 +117,12 @@ class ChannelViewState extends State<ChannelView> with AfterLayoutMixin<ChannelV
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0, top: 8, right: 8),
child: AnimatedOpacity(
opacity: opacity,
duration: Duration.zero,
child: Thumbnail(
width: double.infinity,
height: bannerHeight,
thumbnailUrl: ImageObject.getBestThumbnail(channel!.authorThumbnails)?.url ?? '',
id: 'channel-banner/${widget.channelId}',
decoration: BoxDecoration(color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(10))),
),
child: Thumbnail(
width: double.infinity,
height: 150,
thumbnailUrl: ImageObject.getBestThumbnail(channel!.authorThumbnails)?.url ?? '',
id: 'channel-banner/${widget.channelId}',
decoration: BoxDecoration(color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(10))),
),
Padding(
padding: const EdgeInsets.all(8.0),
Expand All @@ -148,17 +133,12 @@ class ChannelViewState extends State<ChannelView> with AfterLayoutMixin<ChannelV
),
),
Expanded(
child: SingleChildScrollView(
controller: scrollController,
child: Padding(
padding: const EdgeInsets.only(left: 8.0, top: 8.0, right: 8.0),
child: <Widget>[
ChannelInfo(channel: channel!),
ChannelVideosView(channel: channel!),
// ChannelPlayListsView(channelId: channel!.authorId)
][selectedIndex],
),
),
child: <Widget>[
ChannelInfo(key: const ValueKey('info'),channel: channel!),
ChannelVideosView(key: const ValueKey('videos'), channel: channel!, getVideos: service.getChannelVideos,),
ChannelVideosView(key: const ValueKey('streams'),channel: channel!, getVideos: service.getChannelStreams,),
ChannelPlayListsView(key: const ValueKey('playlists'),channelId: channel!.authorId, canDeleteVideos: false)
][selectedIndex],
)
],
),
Expand Down
11 changes: 8 additions & 3 deletions lib/views/channel/info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,14 @@ class ChannelInfoState extends State<ChannelInfo> {
childAspectRatio: getGridAspectRatio(context),
children: widget.channel.latestVideos.map((e) => VideoListItem(video: VideoInList(e.title, e.videoId, e.lengthSeconds, 0, e.author, '', 'authorUrl', 0, '', e.videoThumbnails))).toList()));

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: widgets,
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: widgets,
),
),
);
}
}
Loading

0 comments on commit e11d1ae

Please sign in to comment.