Beta RC1: Video player implemented

master
Özcan Oğuz 3 years ago
parent d47ba1fe92
commit 9dc42635d0
Signed by: ooguz
GPG Key ID: 2D33E2BD3D975818
  1. 0
      lib/data/PeerTube.dart
  2. 38
      lib/i18n.dart
  3. 19
      lib/main.dart
  4. 17
      lib/screens/About.dart
  5. 72
      lib/screens/Chat.dart
  6. 30
      lib/screens/HomePage.dart
  7. 37
      lib/screens/Live.dart
  8. 11
      lib/screens/Settings.dart
  9. 140
      lib/screens/VideoPlayer.dart
  10. 93
      lib/screens/Videos.dart
  11. 119
      lib/widgets/Schedule.dart

@ -0,0 +1,38 @@
import 'package:get/get.dart';
class Messages extends Translations {
@override
Map<String, Map<String, String>> get keys => {
'en_US': {
'chat': 'Chat',
'live': 'Live',
'schedule': 'Schedule',
'videos': 'Videos',
'about_app': 'About ÖzgürKon app',
'app_description': 'Android app for ÖzgürKon, Developed in Flutter.',
'view_readme': 'View README',
'view_changelog': 'View changelog',
'view_license': 'View license',
'contributors': 'Contributors',
'fs_licenses': 'Free software licenses',
'loading': "Loading...",
'next_year': "See you next year!",
},
'tr_TR': {
'chat': 'Sohbet',
'live': 'Canlı',
'schedule': 'Program',
'videos': 'Videolar',
'about_app': 'ÖzgürKon uygulaması hakkında',
'app_description':
'ÖzgürKon için Android uygulaması, Flutter ile geliştirildi.',
'view_readme': "README'yi görüntüle",
'view_changelog': "Değişiklik özetini görüntüle",
'view_license': 'Lisans',
'contributors': 'Katkıda bulunanlar',
'fs_licenses': 'Özgür yazılım lisansları',
'loading': 'Yükleniyor...',
'next_year': "Seneye görüşmek üzere!",
}
};
}

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:material_color_generator/material_color_generator.dart'; import 'package:material_color_generator/material_color_generator.dart';
import 'package:ozgurkon_app/screens/VideoPlayer.dart';
import 'package:ozgurkon_app/screens/HomePage.dart'; import 'package:ozgurkon_app/screens/HomePage.dart';
import 'package:get/get.dart';
import 'i18n.dart';
const ozgurkon_renk = const Color(0xffc03e24); const ozgurkon_renk = const Color(0xffc03e24);
@ -12,12 +15,26 @@ class MyApp extends StatelessWidget {
// This widget is the root of your application. // This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return GetMaterialApp(
title: 'ÖzgürKon', title: 'ÖzgürKon',
theme: ThemeData( theme: ThemeData(
primarySwatch: generateMaterialColor(color: Color(0xffc03e24)), primarySwatch: generateMaterialColor(color: Color(0xffc03e24)),
), ),
home: HomePage(), home: HomePage(),
translations: Messages(),
locale: Get.deviceLocale,
fallbackLocale: Locale('en', 'US'),
initialRoute: '/',
getPages: [
GetPage(
name: '/',
page: () => HomePage(),
),
GetPage(
name: '/video/:uuid',
page: () => VideoView(),
),
],
); );
} }
} }

@ -2,6 +2,7 @@ import 'package:about/about.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
String appName = "ÖzgürKon"; String appName = "ÖzgürKon";
String version = "0.1.1"; String version = "0.1.1";
@ -21,10 +22,10 @@ class MyAbout extends StatelessWidget {
'year': DateTime.now().year.toString(), 'year': DateTime.now().year.toString(),
'author': "Özgür Yazılım Derneği", 'author': "Özgür Yazılım Derneği",
}, },
//title: Text('About'), title: Text('about_app'.tr),
applicationVersion: 'Version {{ version }}, build #{{ buildNumber }}', applicationVersion: 'Version {{ version }}, build #{{ buildNumber }}',
applicationDescription: Text( applicationDescription: Text(
"ÖzgürKon app test", "app_description".tr,
textAlign: TextAlign.justify, textAlign: TextAlign.justify,
), ),
applicationIcon: Image.asset('assets/ozgurkon_disi.png'), applicationIcon: Image.asset('assets/ozgurkon_disi.png'),
@ -32,26 +33,26 @@ class MyAbout extends StatelessWidget {
children: <Widget>[ children: <Widget>[
MarkdownPageListTile( MarkdownPageListTile(
filename: 'README.md', filename: 'README.md',
title: Text('View Readme'), title: Text('view_readme'.tr),
icon: Icon(Icons.all_inclusive), icon: Icon(Icons.all_inclusive),
), ),
MarkdownPageListTile( MarkdownPageListTile(
filename: 'CHANGELOG.md', filename: 'CHANGELOG.md',
title: Text('View Changelog'), title: Text('view_changelog'.tr),
icon: Icon(Icons.view_list), icon: Icon(Icons.view_list),
), ),
MarkdownPageListTile( MarkdownPageListTile(
filename: 'LICENSE.md', filename: 'LICENSE.md',
title: Text('View License'), title: Text('view_license'.tr),
icon: Icon(Icons.description), icon: Icon(Icons.description),
), ),
MarkdownPageListTile( MarkdownPageListTile(
filename: 'CONTRIBUTORS.md', filename: 'CONTRIBUTORS.md',
title: Text('Contributors'), title: Text('contributors'.tr),
icon: Icon(Icons.groups), icon: Icon(Icons.groups),
), ),
LicensesPageListTile( LicensesPageListTile(
title: Text('Free software Licenses'), title: Text('fs_licenses'.tr),
icon: Icon(Icons.favorite), icon: Icon(Icons.favorite),
), ),
], ],
@ -59,7 +60,7 @@ class MyAbout extends StatelessWidget {
if (isIos) { if (isIos) {
return CupertinoApp( return CupertinoApp(
title: 'About this app', title: 'about_app'.tr,
home: aboutPage, home: aboutPage,
theme: CupertinoThemeData( theme: CupertinoThemeData(
brightness: theme.brightness, brightness: theme.brightness,

@ -1,24 +1,78 @@
import 'dart:io'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart'; import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:uuid/uuid.dart';
class ChatPage extends StatefulWidget {
const ChatPage({Key? key}) : super(key: key);
class WebViewExample extends StatefulWidget {
@override @override
WebViewExampleState createState() => WebViewExampleState(); _ChatPageState createState() => _ChatPageState();
} }
class WebViewExampleState extends State<WebViewExample> { class _ChatPageState extends State<ChatPage> {
List<types.Message> _messages = [];
final _user = const types.User(id: '06c33e8b-e835-4736-80f4-63f44b66666c');
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Enable hybrid composition. _loadMessages();
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView(); }
void _addMessage(types.Message message) {
setState(() {
_messages.insert(0, message);
});
}
void _handlePreviewDataFetched(
types.TextMessage message,
types.PreviewData previewData,
) {
final index = _messages.indexWhere((element) => element.id == message.id);
final updatedMessage = _messages[index].copyWith(previewData: previewData);
WidgetsBinding.instance?.addPostFrameCallback((_) {
setState(() {
_messages[index] = updatedMessage;
});
});
}
void _handleSendPressed(types.PartialText message) {
final textMessage = types.TextMessage(
author: _user,
createdAt: DateTime.now().millisecondsSinceEpoch,
id: const Uuid().v4(),
text: message.text,
);
_addMessage(textMessage);
}
void _loadMessages() async {
final response = await rootBundle.loadString('assets/messages.json');
final messages = (jsonDecode(response) as List)
.map((e) => types.Message.fromJson(e as Map<String, dynamic>))
.toList();
setState(() {
_messages = messages;
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WebView( return Scaffold(
initialUrl: 'https://kiwiirc.com/client/chat.freenode.net/%23ozgurkon', body: Chat(
messages: _messages,
onPreviewDataFetched: _handlePreviewDataFetched,
onSendPressed: _handleSendPressed,
user: _user,
),
); );
} }
} }

@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:ozgurkon_app/screens/About.dart'; import 'package:get/get.dart';
import 'package:ozgurkon_app/screens/Live.dart';
import 'package:ozgurkon_app/screens/Chat.dart'; import 'package:ozgurkon_app/screens/Chat.dart';
import 'package:ozgurkon_app/widgets/Schedule.dart'; import 'package:ozgurkon_app/widgets/Schedule.dart';
import 'package:ozgurkon_app/screens/Settings.dart';
import 'package:ozgurkon_app/screens/Videos.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
@override @override
@ -13,32 +16,45 @@ class HomePage extends StatefulWidget {
class _HomeState extends State<HomePage> { class _HomeState extends State<HomePage> {
int _currentIndex = 0; int _currentIndex = 0;
final List<Widget> _children = [ final List<Widget> _children = [
PlaceholderWidget(Colors.white), ChatPage(),
MyAbout(), Live(),
WebViewExample() Schedule(),
VideosListView(),
]; ];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('ÖzgürKon'), title: Text('ÖzgürKon'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Settings()),
);
})
],
), ),
body: _children[_currentIndex], // new body: _children[_currentIndex], // new
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: onTabTapped, // new onTap: onTabTapped, // new
currentIndex: currentIndex:
_currentIndex, // this will be set when a new tab is tapped _currentIndex, // this will be set when a new tab is tapped
items: [ items: [
BottomNavigationBarItem( BottomNavigationBarItem(
icon: new Icon(Icons.chat), icon: new Icon(Icons.chat),
label: 'Chat', label: 'chat'.tr,
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: new Icon(Icons.live_tv), icon: new Icon(Icons.live_tv),
label: 'Live', label: 'live'.tr,
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.date_range), label: 'Schedule') icon: Icon(Icons.date_range), label: 'schedule'.tr),
BottomNavigationBarItem(icon: Icon(Icons.movie), label: 'videos'.tr)
], ],
), ),
); );

@ -1,28 +1,31 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:video_viewer/video_viewer.dart'; import 'package:get/get.dart';
import 'package:flutter_svg_provider/flutter_svg_provider.dart';
class HLSVideoExample extends StatelessWidget { class Live extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<Map<String, VideoSource>>( return Container(
future: VideoSource.fromM3u8PlaylistUrl( width: double.infinity,
"https://sfux-ext.sfux.info/hls/chapter/105/1588724110/1588724110.m3u8", height: double.infinity,
formatter: (quality) => decoration: BoxDecoration(
quality == "Auto" ? "Automatic" : "${quality.split("x").last}p", color: Color(0xFFededed),
image: DecorationImage(
image: Svg(
'assets/flat-mountains.svg',
), ),
builder: (_, data) { alignment: Alignment.bottomCenter,
return data.hasData
? VideoViewer(
source: data.data,
onFullscreenFixLandscape: true,
style: VideoViewerStyle(
thumbnail: Image.network(
"https://play-lh.googleusercontent.com/aA2iky4PH0REWCcPs9Qym2X7e9koaa1RtY-nKkXQsDVU6Ph25_9GkvVuyhS72bwKhN1P",
), ),
), ),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'next_year'.tr,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700),
) )
: CircularProgressIndicator(); ],
}, ),
); );
} }
} }

@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
import 'package:ozgurkon_app/screens/About.dart';
class Settings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: MyAbout(),
);
}
}

@ -0,0 +1,140 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:get/get.dart';
import 'package:fluttericon/entypo_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:better_player/better_player.dart';
class Video {
final String uuid;
final String name;
final String desc;
final String speaker;
final String lang;
final int duration;
final String streamURL;
final String downloadURL;
Video(
{required this.uuid,
required this.name,
required this.desc,
required this.speaker,
required this.lang,
required this.duration,
required this.streamURL,
required this.downloadURL});
factory Video.fromJson(Map<String, dynamic> json) {
return Video(
uuid: json['uuid'],
name: json['name'].split(' | ')[0].split(' by ')[0],
desc: json['description'],
speaker: json['name'].split(' | ')[0].split(' by ')[1],
lang: json['language']['id'],
duration: json['duration'],
streamURL: json['files'][0]['fileUrl'],
downloadURL: json['files'][0]['fileDownloadUrl'],
);
}
}
class VideoView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder<Video>(
future: _fetchVideoDetails(),
builder: (context, snapshot) {
if (snapshot.hasData) {
Video? data = snapshot.data;
return new Scaffold(
appBar: AppBar(
title: Text("Video"),
),
body: new SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[_videoView(data)],
),
));
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
return Container(child: Row(children: [CircularProgressIndicator()]));
},
);
}
Future<Video> _fetchVideoDetails() async {
String uuid = Get.parameters['uuid'] as String;
final String videoDetailAPI =
'https://video.ozgurkon.org/api/v1/videos/' + uuid;
final response = await http.get(Uri.parse(videoDetailAPI));
if (response.statusCode == 200) {
var jsonResponse = json.decode(response.body);
final description =
await http.get(Uri.parse(videoDetailAPI + '/description'));
if (description.statusCode == 200) {
var descriptionResponse = json.decode(description.body);
jsonResponse['description'] = descriptionResponse['description'];
}
return Video.fromJson(jsonResponse);
} else {
throw Exception('Failed to load video details from API');
}
}
Card _videoView(data) {
return Card(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AspectRatio(
aspectRatio: 16 / 9,
child: BetterPlayer.network(
data!.streamURL,
betterPlayerConfiguration: BetterPlayerConfiguration(
aspectRatio: 16 / 9,
autoPlay: true,
),
),
),
ListTile(
title: Text(data.name),
subtitle: Text(data.speaker),
trailing: Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
Icon(Entypo.cc),
Icon(Entypo.cc_by),
Icon(Entypo.cc_sa)
]),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
data.desc,
style: TextStyle(color: Colors.black.withOpacity(0.6)),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
TextButton(
child: const Text('DOWNLOAD'),
onPressed: () {
Get.toNamed('/videoplayer', arguments: data.streamURL);
},
),
const SizedBox(width: 8),
],
),
],
),
);
}
}

@ -0,0 +1,93 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:get/get.dart';
class VideoList {
final String uuid;
final String name;
final String speaker;
final String lang;
final String thumbnail;
VideoList(
{required this.uuid,
required this.name,
required this.speaker,
required this.lang,
required this.thumbnail});
factory VideoList.fromJson(Map<String, dynamic> json) {
return VideoList(
uuid: json['uuid'],
name: json['name'].split(' | ')[0].split(' by ')[0],
speaker: json['name'].split(' | ')[0].split(' by ')[1],
lang: json['language']['id'],
thumbnail: json['thumbnailPath'],
);
}
}
class VideosListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder<List<VideoList>>(
future: _fetchVideos(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<VideoList>? data = snapshot.data;
return _videosListView(data);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
return Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [CircularProgressIndicator()]));
},
);
}
Future<List<VideoList>> _fetchVideos() async {
final videosFrom2020API =
'https://video.ozgurkon.org/api/v1/video-channels/ozgurkon2020/videos';
final response = await http.get(Uri.parse(videosFrom2020API));
if (response.statusCode == 200) {
Map<String, dynamic> jsonResponse = json.decode(response.body);
List videoList = jsonResponse['data'];
return videoList.map((job) => new VideoList.fromJson(job)).toList();
} else {
throw Exception('Failed to load videos from API');
}
}
ListView _videosListView(data) {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return _tile(data[index].uuid, data[index].name, data[index].speaker,
data[index].thumbnail, data[index].lang);
});
}
ListTile _tile(String uuid, String title, String subtitle, String thumbnail,
String lang) =>
ListTile(
title: Text(title,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 20,
)),
subtitle: Text(subtitle),
leading: Image.network('https://video.ozgurkon.org/' + thumbnail),
trailing: lang == "tr" ? Text("🇹🇷") : Text('🇬🇧'),
onTap: () {
print(uuid);
Get.toNamed('/video/$uuid');
},
);
}

@ -1,14 +1,125 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class PlaceholderWidget extends StatelessWidget { import 'dart:async';
final Color color; import 'dart:convert';
PlaceholderWidget(this.color); import 'package:http/http.dart' as http;
import 'package:get/get.dart';
class Schedule extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
flexibleSpace: new Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
new TabBar(
tabs: [
new Tab(
text: "2021-05-29",
),
new Tab(
text: "2021-05-30",
),
],
),
],
),
),
body: TabBarView(
children: [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
],
),
),
);
}
}
class ScheduleItem {
final String time;
final String name;
final List speakers;
final String lang;
ScheduleItem(
{required this.time,
required this.name,
required this.speakers,
required this.lang});
factory ScheduleItem.fromJson(Map<String, dynamic> json) {
return ScheduleItem(
time: json['time'],
name: json['name'],
speakers: json['speakers'],
lang: json['lang'],
);
}
}
class ScheduleView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder<List<ScheduleItem>>(
future: _fetchSchedule(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<ScheduleItem>? data = snapshot.data;
return _scheduleListView(data);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
return Container( return Container(
color: color, child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [CircularProgressIndicator()]));
},
); );
} }
Future<List<ScheduleItem>> _fetchSchedule() async {
final videosFrom2020API =
'https://video.ozgurkon.org/api/v1/video-channels/ozgurkon2020/videos';
final response = await http.get(Uri.parse(videosFrom2020API));
if (response.statusCode == 200) {
Map<String, dynamic> jsonResponse = json.decode(response.body);
List scheduleList = jsonResponse['data'];
return scheduleList.map((job) => new ScheduleItem.fromJson(job)).toList();
} else {
throw Exception('Failed to load schedule from API');
}
}
ListView _scheduleListView(data) {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return _tile(data[index].uuid, data[index].name, data[index].speaker,
data[index].thumbnail, data[index].lang);
});
}
ListTile _tile(String uuid, String title, String subtitle, String thumbnail,
String lang) =>
ListTile(
title: Text(title,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 20,
)),
subtitle: Text(subtitle),
leading: Image.network('https://video.ozgurkon.org/' + thumbnail),
trailing: lang == "tr" ? Text("🇹🇷") : Text('🇬🇧'),
onTap: () {
print(uuid);
Get.toNamed('/video/$uuid');
},
);
} }

Loading…
Cancel
Save