Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions lib/backend/db_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ class DbFactory {
await tx.execute('''
CREATE INDEX index_groups_media_type ON groups(media_type);
''');
}))
..add(SqliteMigration(4, (tx) async {
await tx.execute('''
ALTER TABLE groups
ADD COLUMN enabled integer DEFAULT 1;
''');
await tx.execute('''
CREATE INDEX index_groups_enabled ON groups(enabled);
''');
}));
await migrations.migrate(db);
return db;
Expand Down
59 changes: 55 additions & 4 deletions lib/backend/sql.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,17 @@ class Sql {
? query.split(" ").map((f) => "%$f%").toList()
: ["%$query%"];
var sqlQuery = '''
SELECT * FROM channels
SELECT * FROM channels
WHERE (${getKeywordsSql(keywords.length)})
AND media_type IN (${generatePlaceholders(mediaTypes.length)})
AND source_id IN (${generatePlaceholders(filters.sourceIds!.length)})
AND url IS NOT NULL
''';
List<Object> params = [];
if (filters.enabledGroupIds != null && filters.enabledGroupIds!.isNotEmpty) {
sqlQuery +=
"\nAND (group_id IS NULL OR group_id IN (${generatePlaceholders(filters.enabledGroupIds!.length)}))";
}
if (filters.viewType == ViewType.favorites && filters.seriesId == null) {
sqlQuery += "\nAND favorite = 1";
}
Expand All @@ -181,6 +185,9 @@ class Sql {
params.addAll(keywords);
params.addAll(mediaTypes);
params.addAll(filters.sourceIds!);
if (filters.enabledGroupIds != null && filters.enabledGroupIds!.isNotEmpty) {
params.addAll(filters.enabledGroupIds!);
}
if (filters.seriesId != null) {
params.add(filters.seriesId!);
} else if (filters.groupId != null) {
Expand Down Expand Up @@ -224,10 +231,11 @@ class Sql {
: ["%$query%"];
var mediaTypes = filters.mediaTypes!.map((x) => x.index);
var sqlQuery = '''
SELECT * FROM groups
SELECT * FROM groups
WHERE (${getKeywordsSql(keywords.length)})
AND (media_type IS NULL OR media_type IN (${generatePlaceholders(mediaTypes.length)}))
AND source_id IN (${generatePlaceholders(filters.sourceIds!.length)})
AND enabled = 1
LIMIT ?, ?
''';
List<Object> params = [];
Expand Down Expand Up @@ -375,12 +383,55 @@ class Sql {
static Future<void> setSourceEnabled(bool enabled, int sourceId) async {
var db = await DbFactory.db;
await db.execute('''
UPDATE sources
SET enabled = ?
UPDATE sources
SET enabled = ?
WHERE id = ?
''', [enabled, sourceId]);
}

static Future<void> setGroupEnabled(bool enabled, int groupId) async {
var db = await DbFactory.db;
await db.execute('''
UPDATE groups
SET enabled = ?
WHERE id = ?
''', [enabled ? 1 : 0, groupId]);
}

static Future<void> setAllGroupsEnabled(bool enabled) async {
var db = await DbFactory.db;
await db.execute('''
UPDATE groups
SET enabled = ?
''', [enabled ? 1 : 0]);
}

static Future<List<Map<String, dynamic>>> getGroups() async {
var db = await DbFactory.db;
var results = await db.getAll('''
SELECT g.id, g.name, g.enabled, s.name as source_name
FROM groups g
JOIN sources s ON g.source_id = s.id
ORDER BY s.name, g.name
''');
return results
.map((row) => {
'id': row.columnAt(0) as int,
'name': row.columnAt(1) as String,
'enabled': row.columnAt(2) == 1,
'sourceName': row.columnAt(3) as String,
})
.toList();
}

static Future<List<int>> getEnabledGroupIds() async {
var db = await DbFactory.db;
var results = await db.getAll('''
SELECT id FROM groups WHERE enabled = 1
''');
return results.map((row) => row.columnAt(0) as int).toList();
}

static Future setPosition(int channelId, int seconds) async {
var db = await DbFactory.db;
await db.execute('''
Expand Down
19 changes: 12 additions & 7 deletions lib/channel_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,19 @@ class _ChannelTileState extends State<ChannelTile> {
void initState() {
super.initState();
_focusNode.onKeyEvent = (node, event) {
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.arrowRight) {
if (!FocusScope.of(
context,
).focusInDirection(TraversalDirection.right)) {
widget.onFocusNavbar?.call();
if (event is KeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
if (!FocusScope.of(
context,
).focusInDirection(TraversalDirection.right)) {
widget.onFocusNavbar?.call();
}
return KeyEventResult.handled;
}
if (event.logicalKey == LogicalKeyboardKey.mediaPlayPause) {
favorite();
return KeyEventResult.handled;
}
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
};
Expand Down
5 changes: 5 additions & 0 deletions lib/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class _HomeState extends State<Home> {
widget.home.filters.mediaTypes = (await SettingsService.getSettings())
.getMediaTypes();
}
if (widget.home.filters.enabledGroupIds == null) {
widget.home.filters.enabledGroupIds = await Sql.getEnabledGroupIds();
}
await load();
final String? version = await SettingsService.shouldShowWhatsNew();
if (widget.firstLaunch && version != null) {
Expand Down Expand Up @@ -169,6 +172,7 @@ class _HomeState extends State<Home> {
viewType: type,
mediaTypes: widget.home.filters.mediaTypes,
sourceIds: widget.home.filters.sourceIds,
enabledGroupIds: widget.home.filters.enabledGroupIds,
),
),
),
Expand All @@ -184,6 +188,7 @@ class _HomeState extends State<Home> {
viewType: ViewType.all,
mediaTypes: widget.home.filters.mediaTypes,
sourceIds: widget.home.filters.sourceIds,
enabledGroupIds: widget.home.filters.enabledGroupIds,
),
);
if (widget.home.filters.groupId != null) {
Expand Down
2 changes: 2 additions & 0 deletions lib/models/filters.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Filters {
int page;
int? seriesId;
int? groupId;
List<int>? enabledGroupIds;
bool useKeywords;

Filters({
Expand All @@ -19,6 +20,7 @@ class Filters {
this.page = 1,
this.seriesId,
this.groupId,
this.enabledGroupIds,
this.useKeywords = false,
});
}
172 changes: 172 additions & 0 deletions lib/settings_view.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:open_tv/backend/settings_service.dart';
import 'package:open_tv/backend/sql.dart';
import 'package:open_tv/backend/utils.dart';
Expand Down Expand Up @@ -31,21 +32,65 @@ class SettingsView extends StatefulWidget {
class _SettingsState extends State<SettingsView> {
Settings settings = Settings();
List<Source> sources = [];
List<Map<String, dynamic>> categories = [];
String _categorySearch = '';
bool loading = true;
bool _searchReadOnly = true;
late final FocusNode _searchFocusNode;
@override
void initState() {
super.initState();
_searchFocusNode = FocusNode(
onKeyEvent: (node, event) {
if (event is! KeyDownEvent && event is! KeyRepeatEvent) {
return KeyEventResult.ignored;
}
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
setState(() => _searchReadOnly = true);
node.nextFocus();
return KeyEventResult.handled;
}
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
setState(() => _searchReadOnly = true);
node.previousFocus();
return KeyEventResult.handled;
}
if (event.logicalKey == LogicalKeyboardKey.enter ||
event.logicalKey == LogicalKeyboardKey.select) {
if (_searchReadOnly) {
setState(() => _searchReadOnly = false);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
if (event.logicalKey == LogicalKeyboardKey.escape ||
event.logicalKey == LogicalKeyboardKey.goBack) {
setState(() => _searchReadOnly = true);
node.unfocus();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
);
initAsync();
}

@override
void dispose() {
_searchFocusNode.dispose();
super.dispose();
}

Future<void> initAsync() async {
var results = await Future.wait([
SettingsService.getSettings(),
Sql.getSources(),
Sql.getGroups(),
]);
setState(() {
settings = results[0] as Settings;
sources = results[1] as List<Source>;
categories = results[2] as List<Map<String, dynamic>>;
loading = false;
});
}
Expand Down Expand Up @@ -191,6 +236,79 @@ class _SettingsState extends State<SettingsView> {
);
}

Future<void> toggleCategory(Map<String, dynamic> category) async {
await Error.tryAsyncNoLoading(
() async =>
await Sql.setGroupEnabled(!(category['enabled'] as bool), category['id'] as int),
context,
);
await reloadCategories();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text("Category ${!(category['enabled'] as bool) ? "enabled" : "disabled"}"),
duration: const Duration(milliseconds: 500),
),
);
}

Future<void> setAllCategoriesEnabled(bool enabled) async {
await Error.tryAsyncNoLoading(
() async => await Sql.setAllGroupsEnabled(enabled),
context,
);
await reloadCategories();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("All categories ${enabled ? "enabled" : "disabled"}"),
duration: const Duration(milliseconds: 500),
),
);
}

Future<void> reloadCategories() async {
await Error.tryAsyncNoLoading(
() async => categories = await Sql.getGroups(),
context,
);
setState(() {
categories;
});
}

List<Map<String, dynamic>> get filteredCategories {
if (_categorySearch.isEmpty) return categories;
final query = _categorySearch.toLowerCase();
return categories
.where((c) =>
(c['name'] as String).toLowerCase().contains(query) ||
(c['sourceName'] as String).toLowerCase().contains(query))
.toList();
}

Widget getCategory(Map<String, dynamic> category) {
final enabled = category['enabled'] as bool;
return Card(
margin: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
elevation: 5,
child: ListTile(
leading: Checkbox(
value: enabled,
onChanged: (_) => toggleCategory(category),
),
onTap: () => toggleCategory(category),
contentPadding: const EdgeInsets.only(left: 10),
title: Text(category['name'] as String),
subtitle: Text(category['sourceName'] as String),
),
);
}

Future<void> reloadSources() async {
await Error.tryAsyncNoLoading(
() async => sources = await Sql.getSources(),
Expand Down Expand Up @@ -373,6 +491,60 @@ class _SettingsState extends State<SettingsView> {
),
const SizedBox(height: 10),
...sources.map(getSource),
if (categories.isNotEmpty) ...[
const Divider(),
const Padding(
padding: EdgeInsets.only(left: 10),
child: Text(
'Categories',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
),
),
),
Padding(
padding: const EdgeInsets.only(left: 4),
child: Row(
children: [
Checkbox(
value: categories.every((c) => c['enabled'] as bool),
tristate: true,
onChanged: (value) {
final allEnabled = categories.every((c) => c['enabled'] as bool);
setAllCategoriesEnabled(!allEnabled);
},
),
GestureDetector(
onTap: () {
final allEnabled = categories.every((c) => c['enabled'] as bool);
setAllCategoriesEnabled(!allEnabled);
},
child: const Text('Select All'),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextField(
focusNode: _searchFocusNode,
readOnly: !widget.showNavBar && _searchReadOnly,
decoration: const InputDecoration(
hintText: 'Search categories...',
prefixIcon: Icon(Icons.search),
isDense: true,
),
onChanged: (value) {
setState(() {
_categorySearch = value;
});
},
),
),
const SizedBox(height: 10),
...filteredCategories.map(getCategory),
],
],
),
),
Expand Down