Compare commits

...

10 Commits

Author SHA1 Message Date
6941225e6d fix unloading of tiles causing high internet usage 2023-07-05 22:30:09 +02:00
fc96c5c7d2 use one unified media player for linux and android 2023-07-04 21:20:14 +02:00
7f039396aa save token and settings also in sqlite db 2022-12-01 00:51:22 +01:00
96b8d172ff update dependencies 2022-11-19 22:35:42 +01:00
a7756da713 make theming prettier and darker 2022-11-02 00:36:21 +01:00
02e73eed1b sort items in dialogs 2022-11-02 00:05:07 +01:00
9ef317f0ba outsourced lots of api calls to api folder
centered error message when failed loading video feed
display server url on settings page
2022-10-15 20:28:31 +02:00
cb42db80af load videoprefix on login 2022-09-02 18:42:38 +02:00
7ff0d387b5 update android, gradle and kotlin version
update secure storage and device info
update vlc
2022-09-02 17:46:07 +02:00
d96bc57c09 migrate flutter to 3.3.0 2022-08-31 23:16:56 +02:00
39 changed files with 1027 additions and 859 deletions

View File

@ -1,10 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
# This file should be version controlled.
version:
revision: 37de8a2a9ad217ec9d731f8af0bd7c83e8f4980c
channel: master
revision: f92f44110e87bad5ff168335c36da6f6053036e6
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: f92f44110e87bad5ff168335c36da6f6053036e6
base_revision: f92f44110e87bad5ff168335c36da6f6053036e6
- platform: web
create_revision: f92f44110e87bad5ff168335c36da6f6053036e6
base_revision: f92f44110e87bad5ff168335c36da6f6053036e6
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
compileSdkVersion 33
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8

View File

@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.4.32'
ext.kotlin_version = '1.6.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import '../log/log.dart';
import '../types/actor.dart';
import 'api.dart';
@ -10,3 +11,26 @@ Future<List<Actor>> loadAllActors() async {
final actors = d.map((e) => Actor.fromJson(e)).toList(growable: false);
return actors;
}
Future<List<Actor>> loadActorsOfVideo(int movieId) async {
final data =
await API.query("actor", "getActorsOfVideo", {'MovieId': movieId});
if (data == 'null') {
return [];
}
final d = jsonDecode(data);
List<Actor> dta = (d as List).map((e) => Actor.fromJson(e)).toList();
return dta;
}
Future<void> addActorToVideo(int actorId, int movieId) async {
final data = await API.query(
"actor", "addActorToVideo", {'ActorId': actorId, 'MovieId': movieId});
final d = jsonDecode(data);
if (d["result"] != "success") {
Log.w("couldn't add actor to video");
}
}

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'token.dart';
class TokenException implements Exception {
@ -11,7 +12,7 @@ class TokenException implements Exception {
class API {
static Future<String> query(
String apinode, String action, Object payload) async {
final t = await Token.getInstance().getToken();
final t = await getToken();
if (t != null) {
final resp = await http.post(
Uri.parse(t.domain + '/api/$apinode/$action'),

42
lib/api/settings_api.dart Normal file
View File

@ -0,0 +1,42 @@
import 'dart:convert';
import 'api.dart';
class InitialData {
bool darkMode;
bool password;
String mediacenterName;
String videoPath;
String tvShowPath;
bool tvShowEnabled;
bool fullDeleteEnabled;
InitialData(
this.darkMode,
this.password,
this.mediacenterName,
this.videoPath,
this.tvShowPath,
this.tvShowEnabled,
this.fullDeleteEnabled);
factory InitialData.fromJson(dynamic json) {
return InitialData(
json['DarkMode'] as bool,
json['Pasword'] as bool,
json['MediacenterName'] as String,
json['VideoPath'] as String,
json['TVShowPath'] as String,
json['TVShowEnabled'] as bool,
json['FullDeleteEnabled'] as bool,
);
}
}
Future<InitialData> loadInitialData() async {
final data = await API.query("settings", "loadInitialData", {});
final d = jsonDecode(data);
final video = InitialData.fromJson(d);
return video;
}

23
lib/api/tag_api.dart Normal file
View File

@ -0,0 +1,23 @@
import 'dart:convert';
import '../log/log.dart';
import '../types/tag.dart';
import 'api.dart';
Future<List<Tag>> loadAllTags() async {
final data = await API.query("tags", "getAllTags", {});
final d = (jsonDecode(data) ?? []) as List<dynamic>;
final tags = d.map((e) => Tag.fromJson(e)).toList(growable: false);
return tags;
}
Future<void> addTagToVideo(int tagId, int movieId) async {
final data =
await API.query("tags", "addTag", {'TagId': tagId, 'MovieId': movieId});
final d = jsonDecode(data);
if (d["result"] != "success") {
Log.w("couldn't add actor to video");
}
}

View File

@ -1,9 +1,6 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../log/log.dart';
import 'package:openmediacentermobile/db/settings_db.dart';
class TokenT {
String token;
@ -12,47 +9,11 @@ class TokenT {
TokenT(this.token, this.domain);
}
class Token {
static final Token _token = Token._();
final _storage = const FlutterSecureStorage();
String _tokenval = "";
String _domain = "";
static Token getInstance() {
return _token;
Future<TokenT?> getToken() async {
final settings = await SettingsDB.getInstance().getSettings();
if (settings.token == "" || settings.domain == "") {
return null;
} else {
return TokenT(settings.token, settings.domain);
}
Future<TokenT?> getToken() async {
var completer = Completer<TokenT?>();
if (_tokenval == "" || _domain == "") {
Log.d("reading token store");
WidgetsFlutterBinding.ensureInitialized();
final token = await _storage.read(key: 'jwt');
final domain = await _storage.read(key: 'domain');
// check if value is defined in phone store
if (token != null && domain != null) {
_tokenval = token;
_domain = domain;
completer.complete(TokenT(token, domain));
} else {
Log.d("no token defined");
completer.complete(null);
}
} else {
completer.complete(TokenT(_tokenval, _domain));
}
return completer.future;
}
void setToken(String token, String domain) {
_tokenval = token;
_domain = domain;
_storage.write(key: 'jwt', value: token);
_storage.write(key: 'domain', value: domain);
}
Token._();
}

View File

@ -1,5 +1,11 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import '../log/log.dart';
import '../types/actor.dart';
import '../types/tag.dart';
import '../types/video.dart';
import '../types/video_data.dart';
import 'api.dart';
@ -10,3 +16,55 @@ Future<VideoData> loadVideoData(int videoId) async {
final video = VideoData.fromJson(d);
return video;
}
Future<List<VideoT>> loadVideo(Tag? tag, int filterIdx) async {
final data = await API
.query("video", "getMovies", {'Tag': tag?.tagId ?? 1, 'Sort': filterIdx});
final d = jsonDecode(data);
List<VideoT> dta =
(d['Videos'] as List).map((e) => VideoT.fromJson(e)).toList();
return dta;
}
Future<List<VideoT>> loadShuffledVideos(int nr) async {
final data = await API.query("video", "getRandomMovies",
{'Number': nr, 'Seed': Random().nextInt(0x7fffffff)});
final d = jsonDecode(data);
List<VideoT> dta =
(d['Videos'] as List).map((e) => VideoT.fromJson(e)).toList();
return dta;
}
Future<List<VideoT>> loadVideoByActor(Actor actor) async {
final data =
await API.query("actor", "getActorInfo", {'ActorId': actor.actorId});
final d = jsonDecode(data);
List<VideoT> dta =
(d['Videos'] as List).map((e) => VideoT.fromJson(e)).toList();
return dta;
}
Future<bool> addLike(int movieId) async {
final data = await API.query("video", "addLike", {'MovieId': movieId});
final d = jsonDecode(data);
if (d["result"] != 'success') {
Log.w(d);
}
return d["result"] == 'success';
}
Future<Uint8List> fetchThumbnail(int movieId) async {
final base64str =
await API.query("video", "readThumbnail", {'Movieid': movieId});
return base64Decode(base64str.substring(23));
}

View File

@ -5,13 +5,14 @@ import 'package:openmediacentermobile/log/log.dart';
import 'package:openmediacentermobile/login/login_screen.dart';
import 'drawer/drawer_page.dart';
import 'login/logincontext.dart';
import 'login/login_context.dart';
class AppScrollBehavior extends MaterialScrollBehavior {
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.trackpad
};
}
@ -28,6 +29,8 @@ class App extends StatelessWidget {
return const MaterialApp(home: LoginScreen());
} else {
return MaterialApp(
theme: ThemeData(
appBarTheme: AppBarTheme(backgroundColor: Color(0xff0d0d0d))),
scrollBehavior: AppScrollBehavior(),
home: DrawerPage(
title: 'OpenMediaCenter',

View File

@ -1,43 +1,47 @@
import 'package:flutter/foundation.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart' as nativeffi;
import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart' as webffi;
import '../log/log.dart';
class Db {
late Database _db;
void init() async {
if (kIsWeb) {
Log.i("Database on web is not supported");
return;
}
Future<void> init() async {
String dbpath = 'previews.db';
if (defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS) {
dbpath = join(await getDatabasesPath(), dbpath);
} else if(kIsWeb) {
databaseFactory = webffi.databaseFactoryFfiWeb;
} else {
// Initialize FFI
sqfliteFfiInit();
nativeffi.sqfliteFfiInit();
// Change the default factory
databaseFactory = databaseFactoryFfi;
databaseFactory = nativeffi.databaseFactoryFfi;
}
_db = await openDatabase(
// Set the path to the database. Note: Using the `join` function from the
// `path` package is best practice to ensure the path is correctly
// constructed for each platform.
dbpath,
onCreate: (db, version) {
// Run the CREATE TABLE statement on the database.
return db.execute(
'CREATE TABLE previews(id INTEGER PRIMARY KEY, thumbnail BLOB)',
final batch = db.batch();
batch.execute(
'CREATE TABLE previews(id INTEGER PRIMARY KEY, thumbnail BLOB);',
);
batch.execute(
'CREATE TABLE settings(domain TEXT, token TEXT, videopath TEXT, tilewidth INTEGER);',
);
batch.insert("settings",
{"domain": "", "token": "", "videopath": "", "tilewidth": 0});
return batch.commit();
},
// Set the version. This executes the onCreate function and provides a
// path to perform database upgrades and downgrades.
version: 1,
version: 2,
);
}
@ -50,10 +54,16 @@ class Db {
/// get db size in bytes
Future<int> getDbSize() async {
final int cnt = (await Db().db().rawQuery("pragma page_count;"))[0]
["page_count"] as int;
final batch = _db.batch();
batch.rawQuery("pragma page_count;");
batch.rawQuery("pragma page_size;");
final result = (await batch.commit(noResult: false));
print(result);
final int cnt =
((result[0] as List<Map<String, dynamic>>)[0]["page_count"] as int);
final int pagesize =
(await Db().db().rawQuery("pragma page_size;"))[0]["page_size"] as int;
(result[1] as List<Map<String, dynamic>>)[0]["page_size"] as int;
return cnt * pagesize;
}

51
lib/db/settings_db.dart Normal file
View File

@ -0,0 +1,51 @@
import 'database.dart';
class SettingsT {
String domain;
String token;
String videopath;
int tilewidth;
SettingsT(this.domain, this.token, this.videopath, this.tilewidth);
}
class SettingsDB {
static final SettingsDB _instance = SettingsDB._();
SettingsT _settings = SettingsT("", "", "", 0);
bool _initialized = false;
Future<SettingsT> getSettings() async {
if (!_initialized) {
final result = (await Db().db().query("settings",
where: "1",
columns: ["domain", "token", "videopath", "tilewidth"]))
.first;
_settings = SettingsT(
result["domain"] as String,
result["token"] as String,
result["videopath"] as String,
result["tilewidth"] as int);
}
return _settings;
}
Future<void> setSettings(SettingsT settings) async {
await Db().db().update(
"settings",
{
"domain": settings.domain,
"token": settings.token,
"videopath": settings.videopath,
"tilewidth": settings.tilewidth
},
where: "1");
}
static SettingsDB getInstance() {
return _instance;
}
SettingsDB._();
}

View File

@ -1,10 +1,6 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import '../api/actor_api.dart';
import '../api/api.dart';
import '../log/log.dart';
import '../screen_loading.dart';
import '../types/actor.dart';
@ -19,16 +15,6 @@ class AddActorDialog extends StatefulWidget {
class _AddActorDialogState extends State<AddActorDialog> {
late Future<List<Actor>> actors = loadAllActors();
Future<void> addActorToVideo(int actorId) async {
final data = await API.query("actor", "addActorToVideo",
{'ActorId': actorId, 'MovieId': widget.movieId});
final d = jsonDecode(data);
if (d["result"] != "success") {
Log.w("couldn't add actor to video");
}
}
@override
void initState() {
super.initState();
@ -46,13 +32,15 @@ class _AddActorDialogState extends State<AddActorDialog> {
return Text("Error");
} else if (snapshot.hasData) {
final data = snapshot.data! as List<Actor>;
data.sort((a, b) =>
a.name.toLowerCase().compareTo(b.name.toLowerCase()));
return Column(
mainAxisSize: MainAxisSize.min,
children: data
.map((e) => ListTile(
title: Text(e.name),
onTap: () async {
await addActorToVideo(e.actorId);
await addActorToVideo(e.actorId, widget.movieId);
Navigator.pop(context, e);
},
))

View File

@ -1,9 +1,6 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import '../api/api.dart';
import '../log/log.dart';
import '../api/tag_api.dart';
import '../screen_loading.dart';
import '../types/tag.dart';
@ -18,24 +15,6 @@ class AddTagDialog extends StatefulWidget {
class _AddTagDialogState extends State<AddTagDialog> {
late Future<List<Tag>> tags = loadAllTags();
Future<List<Tag>> loadAllTags() async {
final data = await API.query("tags", "getAllTags", {});
final d = (jsonDecode(data) ?? []) as List<dynamic>;
final tags = d.map((e) => Tag.fromJson(e)).toList(growable: false);
return tags;
}
Future<void> addTagToVideo(int tagId) async {
final data = await API
.query("tags", "addTag", {'TagId': tagId, 'MovieId': widget.movieId});
final d = jsonDecode(data);
if (d["result"] != "success") {
Log.w("couldn't add actor to video");
}
}
@override
void initState() {
super.initState();
@ -53,6 +32,10 @@ class _AddTagDialogState extends State<AddTagDialog> {
return Text("Error");
} else if (snapshot.hasData) {
final data = snapshot.data! as List<Tag>;
data.sort(
(a, b) =>
a.tagName.toLowerCase().compareTo(b.tagName.toLowerCase()),
);
return Column(
mainAxisSize: MainAxisSize.min,
children: data
@ -60,7 +43,7 @@ class _AddTagDialogState extends State<AddTagDialog> {
(e) => ListTile(
title: Text(e.tagName),
onTap: () async {
await addTagToVideo(e.tagId);
await addTagToVideo(e.tagId, widget.movieId);
Navigator.pop(context, e);
},
),

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:openmediacentermobile/navigation/settings_screen.dart';
import '../navigation/actor_screen.dart';
import '../navigation/categorie_screen.dart';
import '../navigation/shufflescreen.dart';
import '../navigation/shuffle_screen.dart';
import '../navigation/video_feed.dart';
import 'drawer_context.dart';

View File

@ -7,52 +7,32 @@ class MyDrawer extends StatelessWidget {
const MyDrawer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Widget build(BuildContext context) => Drawer(
backgroundColor: Color(0xff3f3f3f),
child: ListView(children: [
SizedBox(
height: 75,
),
_listelement('Home', Icons.home, Section.HOME, context),
_listelement('Shuffle', Icons.update, Section.SHUFFLE, context),
_listelement(
'Categories', Icons.category, Section.CATEGORIE, context),
_listelement('Actors', Icons.people, Section.ACTOR, context),
_listelement('Settings', Icons.settings, Section.SETTING, context),
]),
);
Widget _listelement(
String text, IconData icon, Section section, BuildContext context) {
final ctx = DrawerContext.of(context);
return Drawer(
child: ListView(children: [
ListTile(
title: const Text('Home'),
leading: const Icon(Icons.home),
onTap: () {
ctx.onChangePage(Section.HOME);
Navigator.pop(context);
},
),
ListTile(
title: const Text('Shuffle'),
leading: const Icon(Icons.update),
onTap: () {
ctx.onChangePage(Section.SHUFFLE);
Navigator.pop(context);
},
),
ListTile(
title: const Text('Categories'),
leading: const Icon(Icons.category),
onTap: () {
ctx.onChangePage(Section.CATEGORIE);
Navigator.pop(context);
},
),
ListTile(
title: const Text('Actors'),
leading: const Icon(Icons.people),
onTap: () {
ctx.onChangePage(Section.ACTOR);
Navigator.pop(context);
},
),
ListTile(
title: const Text('Settings'),
leading: const Icon(Icons.settings),
onTap: () {
ctx.onChangePage(Section.SETTING);
Navigator.pop(context);
},
),
]),
return ListTile(
title: Text(text, style: TextStyle(color: Color(0xffe9e9e9))),
leading: Icon(icon, color: Color(0xffe9e9e9)),
onTap: () {
ctx.onChangePage(section);
Navigator.pop(context);
},
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../api/token.dart';
import '../log/log.dart';
import 'login_context.dart';
class LoginContainer extends StatefulWidget {
const LoginContainer({Key? key, required this.child}) : super(key: key);
@ -18,22 +19,23 @@ class _LoginContainerState extends State<LoginContainer> {
@override
void initState() {
super.initState();
_init();
}
final token = Token.getInstance();
token.getToken().then((value) {
Log.i("The token value is $value");
if (value != null) {
setState(() {
_loggedIn = true;
_loading = false;
});
} else {
setState(() {
_loggedIn = false;
_loading = false;
});
}
});
void _init() async {
final token = await getToken();
Log.i("The token value is $token");
if (token != null) {
setState(() {
_loggedIn = true;
_loading = false;
});
} else {
setState(() {
_loggedIn = false;
_loading = false;
});
}
}
@override
@ -58,27 +60,3 @@ class _LoginContainerState extends State<LoginContainer> {
);
}
}
class LoginContext extends InheritedWidget {
const LoginContext(
{Key? key,
required Widget child,
required this.loggedIn,
required this.onLoggin})
: super(key: key, child: child);
final bool loggedIn;
final void Function(bool) onLoggin;
static LoginContext of(BuildContext context) {
final LoginContext? result =
context.dependOnInheritedWidgetOfExactType<LoginContext>();
assert(result != null, 'No LoginContext found in context');
return result!;
}
@override
bool updateShouldNotify(LoginContext old) {
return loggedIn != old.loggedIn;
}
}

View File

@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
class LoginContext extends InheritedWidget {
const LoginContext(
{Key? key,
required Widget child,
required this.loggedIn,
required this.onLoggin})
: super(key: key, child: child);
final bool loggedIn;
final void Function(bool) onLoggin;
static LoginContext of(BuildContext context) {
final LoginContext? result =
context.dependOnInheritedWidgetOfExactType<LoginContext>();
assert(result != null, 'No LoginContext found in context');
return result!;
}
@override
bool updateShouldNotify(LoginContext old) {
return loggedIn != old.loggedIn;
}
}

View File

@ -3,9 +3,11 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:openmediacentermobile/api/token.dart';
import 'package:openmediacentermobile/log/log.dart';
import 'package:openmediacentermobile/login/logincontext.dart';
import '../api/settings_api.dart';
import '../db/settings_db.dart';
import '../log/log.dart';
import 'login_context.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({Key? key}) : super(key: key);
@ -39,19 +41,46 @@ class _LoginScreenState extends State<LoginScreen> {
if (resp.statusCode != 200) {
return "error" + resp.body;
// compl.complete(resp.body);
} else {
final json = jsonDecode(resp.body);
final token = json["Token"];
Token.getInstance().setToken(token, domain);
SettingsT settings = await SettingsDB.getInstance().getSettings();
settings.domain = domain;
settings.token = token;
SettingsDB.getInstance().setSettings(settings);
// we need two steps here because we need an authenticated api call for the videopath
settings.videopath = (await loadInitialData()).videoPath;
SettingsDB.getInstance().setSettings(settings);
LoginContext.of(context).onLoggin(true);
return "";
}
}
Future<void> onLoginClick() async {
Log.d("logging in");
final pwd = _passwordTextController.value.text;
final domain = _domainTextController.value.text;
var err = "";
if (domain.startsWith("https://") || domain.startsWith("http://")) {
err = await login(pwd, domain);
if (err.isEmpty) return;
} else {
// try to auto infering domain prefix
err = await login(pwd, "https://" + domain);
if (err.isEmpty) return;
err = await login(pwd, "http://" + domain);
if (err.isEmpty) return;
}
Log.i(err);
setState(() {
error = err;
});
}
@override
void dispose() {
_domainTextController.dispose();
@ -132,33 +161,7 @@ class _LoginScreenState extends State<LoginScreen> {
backgroundColor: const Color(0xff4c505b),
child: IconButton(
color: Colors.white,
onPressed: () async {
Log.d("clickkked");
final pwd =
_passwordTextController.value.text;
final domain =
_domainTextController.value.text;
var err = "";
if (domain.startsWith("https://") ||
domain.startsWith("http://")) {
err = await login(pwd, domain);
if (err.isEmpty) return;
} else {
// try to auto infering domain prefix
err = await login(
pwd, "https://" + domain);
if (err.isEmpty) return;
err = await login(
pwd, "http://" + domain);
if (err.isEmpty) return;
}
Log.i(err);
setState(() {
error = err;
});
},
onPressed: () async => await onLoginClick(),
icon: const Icon(
Icons.arrow_forward,
)),

View File

@ -1,21 +1,27 @@
import "package:dart_vlc/dart_vlc.dart";
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:media_kit/media_kit.dart';
import 'app.dart';
import 'db/database.dart';
import 'log/log.dart';
import 'login/logincontext.dart';
import 'login/login_container.dart';
import 'utils/platform.dart';
void main() async {
Log.i("App init!");
if (isDesktop()) {
DartVLC.initialize();
} else {
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
if (!kIsWeb && !isDesktop()) {
Log.i("init device info");
await loadDeviceInfo();
}
Db().init();
Log.i("Mediakit initialized");
await Db().init();
runApp(Shortcuts(shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.select): ActivateIntent(),

View File

@ -1,11 +1,9 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:openmediacentermobile/preview/tag_tile.dart';
import '../api/tag_api.dart';
import '../preview/tag_tile.dart';
import '../drawer/my_drawer.dart';
import '../screen_loading.dart';
import '../api/api.dart';
import '../types/tag.dart';
class CategorieScreen extends StatefulWidget {
@ -18,18 +16,10 @@ class CategorieScreen extends StatefulWidget {
class _CategorieScreenState extends State<CategorieScreen> {
late Future<List<Tag>> _categories;
Future<List<Tag>> loadVideoData() async {
final data = await API.query("tags", "getAllTags", {});
final d = (jsonDecode(data) ?? []) as List<dynamic>;
final tags = d.map((e) => Tag.fromJson(e)).toList(growable: false);
return tags;
}
@override
void initState() {
super.initState();
_categories = loadVideoData();
_categories = loadAllTags();
}
@override

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:openmediacentermobile/utils/FileFormatter.dart';
import 'package:openmediacentermobile/db/settings_db.dart';
import '../api/token.dart';
import '../db/database.dart';
import '../drawer/my_drawer.dart';
import '../login/logincontext.dart';
import '../login/login_context.dart';
import '../utils/file_formatter.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({Key? key}) : super(key: key);
@ -15,6 +16,7 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> {
int dbsize = 0;
String serverUrl = "";
@override
void initState() {
@ -22,6 +24,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
Db().getDbSize().then((v) => setState(() {
dbsize = v;
}));
getToken().then((value) => setState(() {
serverUrl = value?.domain ?? "unknown";
}));
}
@override
@ -34,6 +40,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
body: Column(
children: [
Text("Current server: $serverUrl"),
ElevatedButton(
onPressed: () async {
await Db().clear();
@ -47,7 +54,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
ElevatedButton(
onPressed: () {
loginCtx.onLoggin(false);
Token.getInstance().setToken("", "");
SettingsDB.getInstance()
.setSettings(SettingsT("", "", "", 0));
Db().clear();
},
child: Text("Logout"))

View File

@ -1,12 +1,9 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import '../api/video_api.dart';
import '../drawer/my_drawer.dart';
import '../preview/preview_grid.dart';
import '../api/api.dart';
import '../utils/platform.dart';
import '../types/video.dart';
class ShuffleScreen extends StatefulWidget {
const ShuffleScreen({Key? key}) : super(key: key);
@ -16,18 +13,6 @@ class ShuffleScreen extends StatefulWidget {
}
class _ShuffleScreenState extends State<ShuffleScreen> {
Future<List<VideoT>> loadData(int nr) async {
final data = await API.query("video", "getRandomMovies",
{'Number': nr, 'Seed': Random().nextInt(0x7fffffff)});
final d = jsonDecode(data);
List<VideoT> dta =
(d['Videos'] as List).map((e) => VideoT.fromJson(e)).toList();
return dta;
}
@override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
@ -38,7 +23,8 @@ class _ShuffleScreenState extends State<ShuffleScreen> {
),
body: PreviewGrid(
videoLoader: () {
return loadData((isTV() ? width ~/ 200 : width ~/ 275) * 2);
return loadShuffledVideos(
(isTV() ? width ~/ 200 : width ~/ 275) * 2);
},
footerBuilder: (state) => Column(
children: [

View File

@ -1,12 +1,9 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import '../api/video_api.dart';
import '../api/api.dart';
import '../drawer/my_drawer.dart';
import '../preview/preview_grid.dart';
import '../types/tag.dart';
import '../types/video.dart';
enum FilterTypes { DATE, LIKES, RANDOM, NAMES, LENGTH }
@ -24,18 +21,6 @@ class VideoFeedState extends State<VideoFeed> {
FilterTypes filterSelection = FilterTypes.DATE;
Key _refreshKey = UniqueKey();
Future<List<VideoT>> loadData() async {
final data = await API.query("video", "getMovies",
{'Tag': widget.tag?.tagId ?? 1, 'Sort': filterSelection.index});
final d = jsonDecode(data);
List<VideoT> dta =
(d['Videos'] as List).map((e) => VideoT.fromJson(e)).toList();
return dta;
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -80,7 +65,7 @@ class VideoFeedState extends State<VideoFeed> {
),
body: PreviewGrid(
key: _refreshKey,
videoLoader: () => loadData(),
videoLoader: () => loadVideo(widget.tag, filterSelection.index),
),
drawer: widget.tag == null ? MyDrawer() : null);
}

View File

@ -1,11 +1,8 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import '../api/api.dart';
import '../api/video_api.dart';
import 'preview_grid.dart';
import '../types/actor.dart';
import '../types/video.dart';
class ActorFeed extends StatefulWidget {
const ActorFeed({Key? key, required this.actor}) : super(key: key);
@ -16,22 +13,10 @@ class ActorFeed extends StatefulWidget {
}
class _ActorFeedState extends State<ActorFeed> {
Future<List<VideoT>> loadData() async {
final data = await API
.query("actor", "getActorInfo", {'ActorId': widget.actor.actorId});
final d = jsonDecode(data);
List<VideoT> dta =
(d['Videos'] as List).map((e) => VideoT.fromJson(e)).toList();
return dta;
}
@override
Widget build(BuildContext context) {
return PreviewGrid(
videoLoader: () => loadData(),
videoLoader: () => loadVideoByActor(widget.actor),
);
}
}

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:openmediacentermobile/preview/actor_feed.dart';
import '../preview/actor_feed.dart';
import '../utils/platform.dart';
import '../types/actor.dart';

View File

@ -3,9 +3,9 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import '../utils/platform.dart';
import '../screen_loading.dart';
import '../types/video.dart';
import '../utils/platform.dart';
import 'preview_tile.dart';
class PreviewGrid extends StatefulWidget {
@ -56,15 +56,15 @@ class _PreviewGridState extends State<PreviewGrid> {
builder:
(BuildContext context, AsyncSnapshot<List<VideoT>> snapshot) {
if (snapshot.hasError) {
return Column(
children: [
Text("Error"),
TextButton(
onPressed: () {
loadData();
},
child: Text("Reload page"))
],
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Error" + snapshot.error.toString()),
TextButton(
onPressed: () => loadData(), child: Text("Reload page"))
],
),
);
} else if (snapshot.hasData) {
return _mainGrid(snapshot.data!, width);
@ -78,50 +78,53 @@ class _PreviewGridState extends State<PreviewGrid> {
Widget _mainGrid(List<VideoT> data, double width) {
return Stack(
children: [
Column(
children: [
if (widget.headerBuilder != null) widget.headerBuilder!(this),
data.length > 0
? Expanded(
child: MasonryGridView.count(
// every tile should be at max 330 pixels long...
crossAxisCount: isTV() ? width ~/ 200 : width ~/ 275,
// crossAxisCount: isTV() ? width ~/ 200 : width ~/ 330,
itemCount: data.length,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
padding: EdgeInsets.all(5),
itemBuilder: (context, index) {
return PreviewTile(
dta: data[index],
onLongPress: (img) {
setState(() {
_previewImage = img;
});
},
onLongPressEnd: () {
setState(() {
_previewImage = null;
});
},
);
},
Container(
color: Color(0xff999999),
child: Column(
children: [
if (widget.headerBuilder != null) widget.headerBuilder!(this),
data.length > 0
? Expanded(
child: MasonryGridView.count(
// every tile should be at max 330 pixels long...
crossAxisCount: isTV() ? width ~/ 200 : width ~/ 275,
// crossAxisCount: isTV() ? width ~/ 200 : width ~/ 330,
itemCount: data.length,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
padding: EdgeInsets.all(5),
itemBuilder: (context, index) {
return PreviewTile(
dta: data[index],
onLongPress: (img) {
setState(() {
_previewImage = img;
});
},
onLongPressEnd: () {
setState(() {
_previewImage = null;
});
},
);
},
),
)
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 32,
),
Icon(Icons.warning_amber, size: 52),
Text("no item available")
],
),
),
)
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 32,
),
Icon(Icons.warning_amber, size: 52),
Text("no item available")
],
),
),
if (widget.footerBuilder != null) widget.footerBuilder!(this),
],
if (widget.footerBuilder != null) widget.footerBuilder!(this),
],
),
),
if (_previewImage != null) ..._buildPreviewImage(),
],

View File

@ -1,15 +1,12 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import '../api/api.dart';
import '../api/video_api.dart';
import '../db/database.dart';
import '../log/log.dart';
import '../utils/platform.dart';
import '../types/video.dart';
import '../utils/platform.dart';
import '../video_screen/videoscreen.dart';
class PreviewTile extends StatefulWidget {
@ -53,23 +50,17 @@ class _PreviewTileState extends State<PreviewTile> {
);
}
Future<Uint8List> _fetchThumbnail(int id) async {
final base64str =
await API.query("video", "readThumbnail", {'Movieid': id});
return base64Decode(base64str.substring(23));
}
Future<Image> loadData() async {
Uint8List data;
final id = widget.dta.id;
if (kIsWeb) {
data = await _fetchThumbnail(id);
data = await fetchThumbnail(id);
} else {
final List<Map<String, dynamic>> prev =
await Db().db().query('previews', where: "id=$id");
if (prev.isEmpty) {
data = await _fetchThumbnail(id);
data = await fetchThumbnail(id);
insert(id, data);
Log.d("Adding $id to db");
} else {
@ -85,7 +76,8 @@ class _PreviewTileState extends State<PreviewTile> {
);
// precache image to avoid loading time to render image
await precacheImage(img.image, context);
if(context.mounted)
await precacheImage(img.image, context);
return img;
}
@ -112,18 +104,28 @@ class _PreviewTileState extends State<PreviewTile> {
child: Stack(
children: [
Container(
color: const Color(0xff3f3f3f),
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: image,
),
const SizedBox(
height: 3,
),
Text(
widget.dta.title,
style: TextStyle(fontSize: isTV() ? 8 : 10.5),
style: TextStyle(
fontSize: isTV() ? 8 : 10.5, color: const Color(0xffe9e9e9)),
overflow: TextOverflow.clip,
maxLines: 1,
),
image
const SizedBox(
height: 3,
),
],
),
color: Color(0x6a94a6ff),
),
Positioned.fill(
child: Material(
@ -156,21 +158,24 @@ class _PreviewTileState extends State<PreviewTile> {
@override
Widget build(BuildContext context) {
return FutureBuilder<Image>(
future: _preview, // a previously-obtained Future<String> or null
builder: (BuildContext context, AsyncSnapshot<Image> snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return _buildLoader();
}
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 200, minWidth: 200),
child: FutureBuilder<Image>(
future: _preview, // a previously-obtained Future<String> or null
builder: (BuildContext context, AsyncSnapshot<Image> snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return _buildLoader();
}
if (snapshot.hasError) {
return Text("Error");
} else if (snapshot.hasData) {
return _buildTile(snapshot.data!);
} else {
return _buildLoader();
}
},
if (snapshot.hasError) {
return Text("Error");
} else if (snapshot.hasData) {
return _buildTile(snapshot.data!);
} else {
return _buildLoader();
}
},
),
);
}
}

View File

@ -18,7 +18,7 @@ class TagTile extends StatelessWidget {
),
);
},
style: ElevatedButton.styleFrom(primary: Color(0x6a94a6ff)),
style: ElevatedButton.styleFrom(backgroundColor: Color(0x6a94a6ff)),
child: SizedBox(
child: Center(child: Text(tag.tagName)),
height: 100,

View File

@ -1,6 +1,5 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import '../api/actor_api.dart';
import '../api/video_api.dart';
import '../dialog/add_actor_dialog.dart';
import '../dialog/add_tag_dialog.dart';
@ -8,9 +7,6 @@ import '../navigation/video_feed.dart';
import '../screen_loading.dart';
import '../types/video_data.dart';
import '../preview/actor_tile.dart';
import '../api/api.dart';
import '../log/log.dart';
import '../types/actor.dart';
class InfoView extends StatefulWidget {
@ -29,7 +25,7 @@ class _InfoViewState extends State<InfoView> {
void initState() {
super.initState();
setState(() {
_data = loadData();
_data = loadActorsOfVideo(widget.videoId);
vdata = loadVideoData(widget.videoId);
});
}
@ -39,19 +35,6 @@ class _InfoViewState extends State<InfoView> {
super.didUpdateWidget(oldWidget);
}
Future<List<Actor>> loadData() async {
final data = await API
.query("actor", "getActorsOfVideo", {'MovieId': widget.videoId});
if (data == 'null') {
return [];
}
final d = jsonDecode(data);
List<Actor> dta = (d as List).map((e) => Actor.fromJson(e)).toList();
return dta;
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
@ -71,15 +54,10 @@ class _InfoViewState extends State<InfoView> {
children: [
IconButton(
onPressed: () async {
final data = await API.query("video", "addLike",
{'MovieId': videoData.movieId});
final d = jsonDecode(data);
if (d["result"] != 'success') {
Log.w(d);
}
setState(() {
vdata = loadVideoData(widget.videoId);
});
if (await addLike(videoData.movieId))
setState(() {
vdata = loadVideoData(widget.videoId);
});
},
icon: Icon(Icons.thumb_up)),
TextButton(
@ -90,9 +68,8 @@ class _InfoViewState extends State<InfoView> {
movieId: videoData.movieId,
),
);
Log.d("finished dialog");
setState(() {
_data = loadData();
_data = loadActorsOfVideo(widget.videoId);
});
},
child: Text("Add Actor"),
@ -105,7 +82,6 @@ class _InfoViewState extends State<InfoView> {
movieId: videoData.movieId,
),
);
Log.d("finished dialog");
setState(() {
vdata = loadVideoData(widget.videoId);
});

View File

@ -1,18 +1,17 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../api/token.dart';
import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:openmediacentermobile/db/settings_db.dart';
import '../api/video_api.dart';
import '../utils/platform.dart';
import '../screen_loading.dart';
import '../types/video.dart';
import '../types/video_data.dart';
import '../utils/platform.dart';
import 'info_view.dart';
import 'videoscreen_desktop.dart'
if (dart.library.html) 'videoscreen_mobile.dart';
import 'videoscreen_mobile.dart';
class VideoScreen extends StatefulWidget {
const VideoScreen({Key? key, required this.metaData}) : super(key: key);
final VideoT metaData;
@ -28,18 +27,19 @@ class _VideoScreenState extends State<VideoScreen> {
PageController _controller = PageController(
initialPage: 0,
);
// Create a [Player] to control playback.
late final player = Player();
// Create a [VideoController] to handle video output from [Player].
late final controller = VideoController(player);
String url = "";
void initPlayer() async {
final videodata = await _videoData;
final token = await Token.getInstance().getToken();
if (token == null) return;
final baseurl = token.domain;
// todo not static middle path
final path = baseurl + "/videos/vids/" + videodata.movieUrl;
final settings = await SettingsDB.getInstance().getSettings();
final path = settings.domain + settings.videopath + videodata.movieUrl;
player.open(Media(path));
url = path;
}
@ -55,6 +55,7 @@ class _VideoScreenState extends State<VideoScreen> {
@override
void dispose() {
super.dispose();
player.dispose();
_controller.dispose();
_appBarTimer?.cancel();
}
@ -94,7 +95,7 @@ class _VideoScreenState extends State<VideoScreen> {
child: GestureDetector(
onPanDown: (details) async {
if (_appBarVisible) {
await Future.delayed(Duration(milliseconds: 100));
await Future.delayed(Duration(milliseconds: 300));
setState(() {
_appBarVisible = false;
});
@ -114,13 +115,7 @@ class _VideoScreenState extends State<VideoScreen> {
controller: _controller,
children: [
Center(
child: isDesktop()
? VideoScreenDesktop(
url: url,
)
: VideoScreenMobile(
url: url,
)),
child: Video(controller: controller)),
InfoView(
videoId: widget.metaData.id,
)
@ -135,7 +130,10 @@ class _VideoScreenState extends State<VideoScreen> {
leading: new IconButton(
icon: new Icon(Icons.arrow_back_ios,
color: Colors.grey),
onPressed: () => Navigator.of(context).pop(),
onPressed: () async {
await player.stop();
Navigator.of(context).pop();
},
),
backgroundColor:
Theme.of(context).primaryColor.withOpacity(0.3),

View File

@ -1,53 +0,0 @@
import 'dart:math';
import 'package:dart_vlc/dart_vlc.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class VideoScreenDesktop extends StatefulWidget {
const VideoScreenDesktop({Key? key, required this.url}) : super(key: key);
final String url;
@override
State<VideoScreenDesktop> createState() => _VideoScreenDesktopState();
}
class _VideoScreenDesktopState extends State<VideoScreenDesktop> {
Player _player = Player(id: Random().nextInt(0x7fffffff));
@override
Widget build(BuildContext context) {
return Video(
player: _player,
scale: 1.0, // default
showControls: true,
playlistLength: 1,
);
}
@override
void initState() {
super.initState();
final media2 = Media.network(widget.url);
_player.open(
media2,
autoStart: true, // default
);
RawKeyboard.instance.addListener((value) {
if (value.logicalKey == LogicalKeyboardKey.arrowRight) {
_player.seek(_player.position.position! + const Duration(seconds: 5));
} else if (value.logicalKey == LogicalKeyboardKey.arrowLeft) {
_player.seek(_player.position.position! + const Duration(seconds: -5));
}
});
}
@override
void dispose() {
super.dispose();
_player.dispose();
}
}

View File

@ -1,59 +0,0 @@
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
class VideoScreenMobile extends StatefulWidget {
const VideoScreenMobile({Key? key, required this.url}) : super(key: key);
final String url;
@override
State<VideoScreenMobile> createState() => _VideoScreenMobileState();
}
class _VideoScreenMobileState extends State<VideoScreenMobile> {
ChewieController? _chewieController;
@override
Widget build(BuildContext context) {
if (_chewieController == null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: const [CircularProgressIndicator(), Text("loading...")],
);
}
return Chewie(
controller: _chewieController!,
);
}
@override
void dispose() {
super.dispose();
_chewieController?.videoPlayerController.dispose();
_chewieController?.dispose();
}
@override
void initState() {
super.initState();
_init();
}
void _init() async {
final VideoPlayerController _controller =
VideoPlayerController.network(widget.url);
await _controller.initialize();
_chewieController = ChewieController(
videoPlayerController: _controller,
autoPlay: true,
looping: true,
allowFullScreen: true,
allowMuting: true,
allowPlaybackSpeedChanging: true,
zoomAndPan: true);
setState(() {});
}
}

View File

@ -6,14 +6,14 @@
#include "generated_plugin_registrant.h"
#include <dart_vlc/dart_vlc_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dart_vlc_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DartVlcPlugin");
dart_vlc_plugin_register_with_registrar(dart_vlc_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
}

View File

@ -3,11 +3,12 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
dart_vlc
flutter_secure_storage_linux
media_kit_libs_linux
media_kit_video
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
media_kit_native_event_loop
)
set(PLUGIN_BUNDLED_LIBRARIES)

File diff suppressed because it is too large Load Diff

View File

@ -34,17 +34,23 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
flutter_secure_storage: ^5.0.2
logger: ^1.1.0
http: ^0.13.4
flutter_staggered_grid_view: ^0.6.1
dart_vlc: ^0.1.9
device_info_plus: ^3.2.3
video_player: ^2.3.0
chewie: ^1.3.2
device_info_plus: ^8.0.0
sqflite: ^2.0.3+1
path: ^1.8.1
sqflite_common_ffi: ^2.1.1+1
sqflite_common_ffi_web: '^0.3.6'
media_kit: ^1.0.2 # Primary package.
media_kit_video: ^1.0.2 # For video rendering.
media_kit_native_event_loop: ^1.0.6 # Support for higher number of concurrent instances & better performance.
media_kit_libs_android_video: ^1.1.1 # Android package for video native libraries.
media_kit_libs_linux: ^1.0.2 # GNU/Linux dependency package.
dev_dependencies:
flutter_test: