@@ -581,7 +581,8 @@
|
||||
"lateNight": "Sen aften",
|
||||
"whatsOn": "Hvad der kører",
|
||||
"watchChannel": "Se kanal",
|
||||
"favorites": "Favoritter"
|
||||
"favorites": "Favoritter",
|
||||
"reorderFavorites": "Omarranger favoritter"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Samlinger",
|
||||
|
||||
@@ -581,7 +581,8 @@
|
||||
"lateNight": "Spätnacht",
|
||||
"whatsOn": "Jetzt im TV",
|
||||
"watchChannel": "Kanal ansehen",
|
||||
"favorites": "Favoriten"
|
||||
"favorites": "Favoriten",
|
||||
"reorderFavorites": "Favoriten sortieren"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
|
||||
@@ -581,7 +581,8 @@
|
||||
"lateNight": "Late Night",
|
||||
"whatsOn": "What's On",
|
||||
"watchChannel": "Watch Channel",
|
||||
"favorites": "Favorites"
|
||||
"favorites": "Favorites",
|
||||
"reorderFavorites": "Reorder Favorites"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Collections",
|
||||
|
||||
@@ -581,7 +581,8 @@
|
||||
"lateNight": "Trasnoche",
|
||||
"whatsOn": "En emisión",
|
||||
"watchChannel": "Ver canal",
|
||||
"favorites": "Favoritos"
|
||||
"favorites": "Favoritos",
|
||||
"reorderFavorites": "Reordenar favoritos"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Colecciones",
|
||||
|
||||
@@ -581,7 +581,8 @@
|
||||
"lateNight": "Nuit tardive",
|
||||
"whatsOn": "En ce moment",
|
||||
"watchChannel": "Regarder la chaîne",
|
||||
"favorites": "Favoris"
|
||||
"favorites": "Favoris",
|
||||
"reorderFavorites": "Réorganiser les favoris"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Collections",
|
||||
|
||||
@@ -581,7 +581,8 @@
|
||||
"lateNight": "Notte tarda",
|
||||
"whatsOn": "In onda ora",
|
||||
"watchChannel": "Guarda canale",
|
||||
"favorites": "Preferiti"
|
||||
"favorites": "Preferiti",
|
||||
"reorderFavorites": "Riordina preferiti"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Download",
|
||||
|
||||
@@ -581,7 +581,8 @@
|
||||
"lateNight": "深夜",
|
||||
"whatsOn": "放送中",
|
||||
"watchChannel": "チャンネルを視聴",
|
||||
"favorites": "お気に入り"
|
||||
"favorites": "お気に入り",
|
||||
"reorderFavorites": "お気に入りを並べ替え"
|
||||
},
|
||||
"collections": {
|
||||
"title": "コレクション",
|
||||
|
||||
@@ -581,7 +581,8 @@
|
||||
"lateNight": "심야 방송",
|
||||
"whatsOn": "지금 방송 중",
|
||||
"watchChannel": "채널 시청",
|
||||
"favorites": "즐겨찾기"
|
||||
"favorites": "즐겨찾기",
|
||||
"reorderFavorites": "즐겨찾기 순서 변경"
|
||||
},
|
||||
"collections": {
|
||||
"title": "컬렉션",
|
||||
|
||||
@@ -581,7 +581,8 @@
|
||||
"lateNight": "Sen kveld",
|
||||
"whatsOn": "Hva går nå",
|
||||
"watchChannel": "Se kanal",
|
||||
"favorites": "Favoritter"
|
||||
"favorites": "Favoritter",
|
||||
"reorderFavorites": "Endre rekkefølge på favoritter"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Samlinger",
|
||||
|
||||
@@ -581,7 +581,8 @@
|
||||
"lateNight": "Late avond",
|
||||
"whatsOn": "Nu op TV",
|
||||
"watchChannel": "Kanaal bekijken",
|
||||
"favorites": "Favorieten"
|
||||
"favorites": "Favorieten",
|
||||
"reorderFavorites": "Favorieten herordenen"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
|
||||
@@ -581,7 +581,8 @@
|
||||
"lateNight": "Późna noc",
|
||||
"whatsOn": "Co leci",
|
||||
"watchChannel": "Oglądaj kanał",
|
||||
"favorites": "Ulubione"
|
||||
"favorites": "Ulubione",
|
||||
"reorderFavorites": "Zmień kolejność ulubionych"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Kolekcje",
|
||||
|
||||
@@ -581,7 +581,8 @@
|
||||
"lateNight": "Madrugada",
|
||||
"whatsOn": "O que Está Passando",
|
||||
"watchChannel": "Assistir Canal",
|
||||
"favorites": "Favoritos"
|
||||
"favorites": "Favoritos",
|
||||
"reorderFavorites": "Reordenar favoritos"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Coleções",
|
||||
|
||||
@@ -581,7 +581,8 @@
|
||||
"lateNight": "Поздний вечер",
|
||||
"whatsOn": "Что идёт",
|
||||
"watchChannel": "Смотреть канал",
|
||||
"favorites": "Избранное"
|
||||
"favorites": "Избранное",
|
||||
"reorderFavorites": "Изменить порядок избранного"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Коллекции",
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 15
|
||||
/// Strings: 12300 (820 per locale)
|
||||
/// Strings: 12315 (821 per locale)
|
||||
///
|
||||
/// Built on 2026-03-31 at 11:48 UTC
|
||||
/// Built on 2026-04-01 at 03:58 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
||||
@@ -814,6 +814,7 @@ class _TranslationsLiveTvDa implements TranslationsLiveTvEn {
|
||||
@override String get whatsOn => 'Hvad der kører';
|
||||
@override String get watchChannel => 'Se kanal';
|
||||
@override String get favorites => 'Favoritter';
|
||||
@override String get reorderFavorites => 'Omarranger favoritter';
|
||||
}
|
||||
|
||||
// Path: collections
|
||||
@@ -1839,6 +1840,7 @@ extension on TranslationsDa {
|
||||
'liveTv.whatsOn' => 'Hvad der kører',
|
||||
'liveTv.watchChannel' => 'Se kanal',
|
||||
'liveTv.favorites' => 'Favoritter',
|
||||
'liveTv.reorderFavorites' => 'Omarranger favoritter',
|
||||
'collections.title' => 'Samlinger',
|
||||
'collections.collection' => 'Samling',
|
||||
'collections.empty' => 'Samlingen er tom',
|
||||
|
||||
@@ -814,6 +814,7 @@ class _TranslationsLiveTvDe implements TranslationsLiveTvEn {
|
||||
@override String get whatsOn => 'Jetzt im TV';
|
||||
@override String get watchChannel => 'Kanal ansehen';
|
||||
@override String get favorites => 'Favoriten';
|
||||
@override String get reorderFavorites => 'Favoriten sortieren';
|
||||
}
|
||||
|
||||
// Path: downloads
|
||||
@@ -1839,6 +1840,7 @@ extension on TranslationsDe {
|
||||
'liveTv.whatsOn' => 'Jetzt im TV',
|
||||
'liveTv.watchChannel' => 'Kanal ansehen',
|
||||
'liveTv.favorites' => 'Favoriten',
|
||||
'liveTv.reorderFavorites' => 'Favoriten sortieren',
|
||||
'downloads.title' => 'Downloads',
|
||||
'downloads.manage' => 'Verwalten',
|
||||
'downloads.tvShows' => 'Serien',
|
||||
|
||||
@@ -1786,6 +1786,9 @@ class TranslationsLiveTvEn {
|
||||
|
||||
/// en: 'Favorites'
|
||||
String get favorites => 'Favorites';
|
||||
|
||||
/// en: 'Reorder Favorites'
|
||||
String get reorderFavorites => 'Reorder Favorites';
|
||||
}
|
||||
|
||||
// Path: collections
|
||||
@@ -3486,6 +3489,7 @@ extension on Translations {
|
||||
'liveTv.whatsOn' => 'What\'s On',
|
||||
'liveTv.watchChannel' => 'Watch Channel',
|
||||
'liveTv.favorites' => 'Favorites',
|
||||
'liveTv.reorderFavorites' => 'Reorder Favorites',
|
||||
'collections.title' => 'Collections',
|
||||
'collections.collection' => 'Collection',
|
||||
'collections.empty' => 'Collection is empty',
|
||||
|
||||
@@ -814,6 +814,7 @@ class _TranslationsLiveTvEs implements TranslationsLiveTvEn {
|
||||
@override String get whatsOn => 'En emisión';
|
||||
@override String get watchChannel => 'Ver canal';
|
||||
@override String get favorites => 'Favoritos';
|
||||
@override String get reorderFavorites => 'Reordenar favoritos';
|
||||
}
|
||||
|
||||
// Path: collections
|
||||
@@ -1839,6 +1840,7 @@ extension on TranslationsEs {
|
||||
'liveTv.whatsOn' => 'En emisión',
|
||||
'liveTv.watchChannel' => 'Ver canal',
|
||||
'liveTv.favorites' => 'Favoritos',
|
||||
'liveTv.reorderFavorites' => 'Reordenar favoritos',
|
||||
'collections.title' => 'Colecciones',
|
||||
'collections.collection' => 'Colección',
|
||||
'collections.empty' => 'La colección está vacía',
|
||||
|
||||
@@ -814,6 +814,7 @@ class _TranslationsLiveTvFr implements TranslationsLiveTvEn {
|
||||
@override String get whatsOn => 'En ce moment';
|
||||
@override String get watchChannel => 'Regarder la chaîne';
|
||||
@override String get favorites => 'Favoris';
|
||||
@override String get reorderFavorites => 'Réorganiser les favoris';
|
||||
}
|
||||
|
||||
// Path: collections
|
||||
@@ -1839,6 +1840,7 @@ extension on TranslationsFr {
|
||||
'liveTv.whatsOn' => 'En ce moment',
|
||||
'liveTv.watchChannel' => 'Regarder la chaîne',
|
||||
'liveTv.favorites' => 'Favoris',
|
||||
'liveTv.reorderFavorites' => 'Réorganiser les favoris',
|
||||
'collections.title' => 'Collections',
|
||||
'collections.collection' => 'Collection',
|
||||
'collections.empty' => 'La collection est vide',
|
||||
|
||||
@@ -814,6 +814,7 @@ class _TranslationsLiveTvIt implements TranslationsLiveTvEn {
|
||||
@override String get whatsOn => 'In onda ora';
|
||||
@override String get watchChannel => 'Guarda canale';
|
||||
@override String get favorites => 'Preferiti';
|
||||
@override String get reorderFavorites => 'Riordina preferiti';
|
||||
}
|
||||
|
||||
// Path: downloads
|
||||
@@ -1839,6 +1840,7 @@ extension on TranslationsIt {
|
||||
'liveTv.whatsOn' => 'In onda ora',
|
||||
'liveTv.watchChannel' => 'Guarda canale',
|
||||
'liveTv.favorites' => 'Preferiti',
|
||||
'liveTv.reorderFavorites' => 'Riordina preferiti',
|
||||
'downloads.title' => 'Download',
|
||||
'downloads.manage' => 'Gestisci',
|
||||
'downloads.tvShows' => 'Serie TV',
|
||||
|
||||
@@ -814,6 +814,7 @@ class _TranslationsLiveTvJa implements TranslationsLiveTvEn {
|
||||
@override String get whatsOn => '放送中';
|
||||
@override String get watchChannel => 'チャンネルを視聴';
|
||||
@override String get favorites => 'お気に入り';
|
||||
@override String get reorderFavorites => 'お気に入りを並べ替え';
|
||||
}
|
||||
|
||||
// Path: collections
|
||||
@@ -1839,6 +1840,7 @@ extension on TranslationsJa {
|
||||
'liveTv.whatsOn' => '放送中',
|
||||
'liveTv.watchChannel' => 'チャンネルを視聴',
|
||||
'liveTv.favorites' => 'お気に入り',
|
||||
'liveTv.reorderFavorites' => 'お気に入りを並べ替え',
|
||||
'collections.title' => 'コレクション',
|
||||
'collections.collection' => 'コレクション',
|
||||
'collections.empty' => 'コレクションは空です',
|
||||
|
||||
@@ -814,6 +814,7 @@ class _TranslationsLiveTvKo implements TranslationsLiveTvEn {
|
||||
@override String get whatsOn => '지금 방송 중';
|
||||
@override String get watchChannel => '채널 시청';
|
||||
@override String get favorites => '즐겨찾기';
|
||||
@override String get reorderFavorites => '즐겨찾기 순서 변경';
|
||||
}
|
||||
|
||||
// Path: collections
|
||||
@@ -1839,6 +1840,7 @@ extension on TranslationsKo {
|
||||
'liveTv.whatsOn' => '지금 방송 중',
|
||||
'liveTv.watchChannel' => '채널 시청',
|
||||
'liveTv.favorites' => '즐겨찾기',
|
||||
'liveTv.reorderFavorites' => '즐겨찾기 순서 변경',
|
||||
'collections.title' => '컬렉션',
|
||||
'collections.collection' => '컬렉션',
|
||||
'collections.empty' => '컬렉션이 비어 있습니다',
|
||||
|
||||
@@ -814,6 +814,7 @@ class _TranslationsLiveTvNb implements TranslationsLiveTvEn {
|
||||
@override String get whatsOn => 'Hva går nå';
|
||||
@override String get watchChannel => 'Se kanal';
|
||||
@override String get favorites => 'Favoritter';
|
||||
@override String get reorderFavorites => 'Endre rekkefølge på favoritter';
|
||||
}
|
||||
|
||||
// Path: collections
|
||||
@@ -1839,6 +1840,7 @@ extension on TranslationsNb {
|
||||
'liveTv.whatsOn' => 'Hva går nå',
|
||||
'liveTv.watchChannel' => 'Se kanal',
|
||||
'liveTv.favorites' => 'Favoritter',
|
||||
'liveTv.reorderFavorites' => 'Endre rekkefølge på favoritter',
|
||||
'collections.title' => 'Samlinger',
|
||||
'collections.collection' => 'Samling',
|
||||
'collections.empty' => 'Samlingen er tom',
|
||||
|
||||
@@ -814,6 +814,7 @@ class _TranslationsLiveTvNl implements TranslationsLiveTvEn {
|
||||
@override String get whatsOn => 'Nu op TV';
|
||||
@override String get watchChannel => 'Kanaal bekijken';
|
||||
@override String get favorites => 'Favorieten';
|
||||
@override String get reorderFavorites => 'Favorieten herordenen';
|
||||
}
|
||||
|
||||
// Path: downloads
|
||||
@@ -1839,6 +1840,7 @@ extension on TranslationsNl {
|
||||
'liveTv.whatsOn' => 'Nu op TV',
|
||||
'liveTv.watchChannel' => 'Kanaal bekijken',
|
||||
'liveTv.favorites' => 'Favorieten',
|
||||
'liveTv.reorderFavorites' => 'Favorieten herordenen',
|
||||
'downloads.title' => 'Downloads',
|
||||
'downloads.manage' => 'Beheren',
|
||||
'downloads.tvShows' => 'Series',
|
||||
|
||||
@@ -814,6 +814,7 @@ class _TranslationsLiveTvPl implements TranslationsLiveTvEn {
|
||||
@override String get whatsOn => 'Co leci';
|
||||
@override String get watchChannel => 'Oglądaj kanał';
|
||||
@override String get favorites => 'Ulubione';
|
||||
@override String get reorderFavorites => 'Zmień kolejność ulubionych';
|
||||
}
|
||||
|
||||
// Path: collections
|
||||
@@ -1839,6 +1840,7 @@ extension on TranslationsPl {
|
||||
'liveTv.whatsOn' => 'Co leci',
|
||||
'liveTv.watchChannel' => 'Oglądaj kanał',
|
||||
'liveTv.favorites' => 'Ulubione',
|
||||
'liveTv.reorderFavorites' => 'Zmień kolejność ulubionych',
|
||||
'collections.title' => 'Kolekcje',
|
||||
'collections.collection' => 'Kolekcja',
|
||||
'collections.empty' => 'Kolekcja jest pusta',
|
||||
|
||||
@@ -814,6 +814,7 @@ class _TranslationsLiveTvPt implements TranslationsLiveTvEn {
|
||||
@override String get whatsOn => 'O que Está Passando';
|
||||
@override String get watchChannel => 'Assistir Canal';
|
||||
@override String get favorites => 'Favoritos';
|
||||
@override String get reorderFavorites => 'Reordenar favoritos';
|
||||
}
|
||||
|
||||
// Path: collections
|
||||
@@ -1839,6 +1840,7 @@ extension on TranslationsPt {
|
||||
'liveTv.whatsOn' => 'O que Está Passando',
|
||||
'liveTv.watchChannel' => 'Assistir Canal',
|
||||
'liveTv.favorites' => 'Favoritos',
|
||||
'liveTv.reorderFavorites' => 'Reordenar favoritos',
|
||||
'collections.title' => 'Coleções',
|
||||
'collections.collection' => 'Coleção',
|
||||
'collections.empty' => 'A coleção está vazia',
|
||||
|
||||
@@ -814,6 +814,7 @@ class _TranslationsLiveTvRu implements TranslationsLiveTvEn {
|
||||
@override String get whatsOn => 'Что идёт';
|
||||
@override String get watchChannel => 'Смотреть канал';
|
||||
@override String get favorites => 'Избранное';
|
||||
@override String get reorderFavorites => 'Изменить порядок избранного';
|
||||
}
|
||||
|
||||
// Path: collections
|
||||
@@ -1839,6 +1840,7 @@ extension on TranslationsRu {
|
||||
'liveTv.whatsOn' => 'Что идёт',
|
||||
'liveTv.watchChannel' => 'Смотреть канал',
|
||||
'liveTv.favorites' => 'Избранное',
|
||||
'liveTv.reorderFavorites' => 'Изменить порядок избранного',
|
||||
'collections.title' => 'Коллекции',
|
||||
'collections.collection' => 'Коллекция',
|
||||
'collections.empty' => 'Коллекция пуста',
|
||||
|
||||
@@ -814,6 +814,7 @@ class _TranslationsLiveTvSv implements TranslationsLiveTvEn {
|
||||
@override String get whatsOn => 'På TV nu';
|
||||
@override String get watchChannel => 'Titta på kanal';
|
||||
@override String get favorites => 'Favoriter';
|
||||
@override String get reorderFavorites => 'Ordna om favoriter';
|
||||
}
|
||||
|
||||
// Path: downloads
|
||||
@@ -1839,6 +1840,7 @@ extension on TranslationsSv {
|
||||
'liveTv.whatsOn' => 'På TV nu',
|
||||
'liveTv.watchChannel' => 'Titta på kanal',
|
||||
'liveTv.favorites' => 'Favoriter',
|
||||
'liveTv.reorderFavorites' => 'Ordna om favoriter',
|
||||
'downloads.title' => 'Nedladdningar',
|
||||
'downloads.manage' => 'Hantera',
|
||||
'downloads.tvShows' => 'TV-serier',
|
||||
|
||||
@@ -814,6 +814,7 @@ class _TranslationsLiveTvZh implements TranslationsLiveTvEn {
|
||||
@override String get whatsOn => '正在播出';
|
||||
@override String get watchChannel => '观看频道';
|
||||
@override String get favorites => '收藏';
|
||||
@override String get reorderFavorites => '重新排序收藏';
|
||||
}
|
||||
|
||||
// Path: downloads
|
||||
@@ -1839,6 +1840,7 @@ extension on TranslationsZh {
|
||||
'liveTv.whatsOn' => '正在播出',
|
||||
'liveTv.watchChannel' => '观看频道',
|
||||
'liveTv.favorites' => '收藏',
|
||||
'liveTv.reorderFavorites' => '重新排序收藏',
|
||||
'downloads.title' => '下载',
|
||||
'downloads.manage' => '管理',
|
||||
'downloads.tvShows' => '电视剧',
|
||||
|
||||
@@ -581,7 +581,8 @@
|
||||
"lateNight": "Sen kväll",
|
||||
"whatsOn": "På TV nu",
|
||||
"watchChannel": "Titta på kanal",
|
||||
"favorites": "Favoriter"
|
||||
"favorites": "Favoriter",
|
||||
"reorderFavorites": "Ordna om favoriter"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Nedladdningar",
|
||||
|
||||
@@ -581,7 +581,8 @@
|
||||
"lateNight": "深夜",
|
||||
"whatsOn": "正在播出",
|
||||
"watchChannel": "观看频道",
|
||||
"favorites": "收藏"
|
||||
"favorites": "收藏",
|
||||
"reorderFavorites": "重新排序收藏"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "下载",
|
||||
|
||||
@@ -14,6 +14,8 @@ import '../../utils/app_logger.dart';
|
||||
import '../../utils/desktop_window_padding.dart';
|
||||
import '../../utils/platform_detector.dart';
|
||||
import '../../widgets/app_icon.dart';
|
||||
import '../../widgets/overlay_sheet.dart';
|
||||
import 'reorder_favorites_sheet.dart';
|
||||
import 'tabs/guide_tab.dart';
|
||||
import 'tabs/whats_on_tab.dart';
|
||||
|
||||
@@ -50,7 +52,11 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
||||
|
||||
List<LiveTvChannel> get _filteredChannels {
|
||||
if (!_showFavoritesOnly || _favoriteChannelIds.isEmpty) return _channels;
|
||||
return _channels.where((c) => _favoriteChannelIds.contains(c.key)).toList();
|
||||
final channelMap = {for (final c in _channels) c.key: c};
|
||||
return [
|
||||
for (final fav in _favoriteChannels)
|
||||
if (channelMap.containsKey(fav.id)) channelMap[fav.id]!,
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -233,7 +239,6 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
||||
}
|
||||
|
||||
void _toggleFavorite(LiveTvChannel channel) {
|
||||
final multiServer = context.read<MultiServerProvider>();
|
||||
final source = _favoriteSourceByServer[channel.serverId] ?? '';
|
||||
|
||||
setState(() {
|
||||
@@ -246,7 +251,37 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
||||
}
|
||||
});
|
||||
|
||||
// Persist to server (fire-and-forget, optimistic UI)
|
||||
_persistFavorites();
|
||||
}
|
||||
|
||||
void _showReorderFavorites() {
|
||||
final channelMap = {for (final c in _channels) c.key: c};
|
||||
|
||||
OverlaySheetController.showAdaptive(
|
||||
context,
|
||||
builder: (sheetContext) => ReorderFavoritesSheet(
|
||||
favorites: List.from(_favoriteChannels),
|
||||
channelMap: channelMap,
|
||||
onReorder: (reordered) {
|
||||
setState(() {
|
||||
_favoriteChannels = reordered;
|
||||
_favoriteChannelIds = reordered.map((f) => f.id).toSet();
|
||||
});
|
||||
_persistFavorites();
|
||||
},
|
||||
onRemove: (removed) {
|
||||
setState(() {
|
||||
_favoriteChannels = _favoriteChannels.where((f) => f.id != removed.id).toList();
|
||||
_favoriteChannelIds = Set.from(_favoriteChannelIds)..remove(removed.id);
|
||||
});
|
||||
_persistFavorites();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _persistFavorites() {
|
||||
final multiServer = context.read<MultiServerProvider>();
|
||||
for (final serverInfo in multiServer.liveTvServers) {
|
||||
final client = multiServer.getClientForServer(serverInfo.serverId);
|
||||
if (client != null) {
|
||||
@@ -322,6 +357,12 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
||||
tooltip: t.liveTv.favorites,
|
||||
onPressed: _toggleFavoritesFilter,
|
||||
),
|
||||
if (_showFavoritesOnly && _favoriteChannels.length > 1)
|
||||
FocusableAction(
|
||||
icon: Symbols.swap_vert_rounded,
|
||||
tooltip: t.liveTv.reorderFavorites,
|
||||
onPressed: _showReorderFavorites,
|
||||
),
|
||||
FocusableAction(
|
||||
icon: Symbols.refresh_rounded,
|
||||
tooltip: t.liveTv.reloadGuide,
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../focus/dpad_navigator.dart';
|
||||
import '../../focus/focus_theme.dart';
|
||||
import '../../focus/input_mode_tracker.dart';
|
||||
import '../../focus/key_event_utils.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../models/livetv_channel.dart';
|
||||
import '../../providers/multi_server_provider.dart';
|
||||
import '../../widgets/app_icon.dart';
|
||||
import '../../widgets/bottom_sheet_header.dart';
|
||||
import '../../widgets/overlay_sheet.dart';
|
||||
import '../../widgets/plex_optimized_image.dart';
|
||||
|
||||
class ReorderFavoritesSheet extends StatefulWidget {
|
||||
final List<FavoriteChannel> favorites;
|
||||
final Map<String, LiveTvChannel> channelMap;
|
||||
final void Function(List<FavoriteChannel>) onReorder;
|
||||
final void Function(FavoriteChannel) onRemove;
|
||||
|
||||
const ReorderFavoritesSheet({
|
||||
super.key,
|
||||
required this.favorites,
|
||||
required this.channelMap,
|
||||
required this.onReorder,
|
||||
required this.onRemove,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ReorderFavoritesSheet> createState() => _ReorderFavoritesSheetState();
|
||||
}
|
||||
|
||||
class _ReorderFavoritesSheetState extends State<ReorderFavoritesSheet> {
|
||||
late List<FavoriteChannel> _tempFavorites;
|
||||
|
||||
// Keyboard navigation state
|
||||
int _focusedIndex = 0;
|
||||
int _focusedColumn = 0; // 0 = row, 1 = remove button
|
||||
int? _movingIndex;
|
||||
int? _originalIndex;
|
||||
List<FavoriteChannel>? _originalOrder;
|
||||
final FocusNode _listFocusNode = FocusNode();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
bool _backKeyDownSeen = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tempFavorites = List.from(widget.favorites);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_listFocusNode.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _ensureFocusedVisible() {
|
||||
if (!_scrollController.hasClients) return;
|
||||
|
||||
const double itemHeight = 72.0;
|
||||
const double listTopPadding = 8.0;
|
||||
final double targetTop = listTopPadding + (_focusedIndex * itemHeight);
|
||||
final double targetBottom = targetTop + itemHeight;
|
||||
|
||||
final double viewportTop = _scrollController.offset;
|
||||
final double viewportHeight = _scrollController.position.viewportDimension;
|
||||
final double viewportBottom = viewportTop + viewportHeight;
|
||||
|
||||
if (targetTop >= viewportTop && targetBottom <= viewportBottom) return;
|
||||
|
||||
final double destination = (targetTop - viewportHeight * 0.25).clamp(
|
||||
0.0,
|
||||
_scrollController.position.maxScrollExtent,
|
||||
);
|
||||
|
||||
_scrollController.animateTo(destination, duration: const Duration(milliseconds: 150), curve: Curves.easeOut);
|
||||
}
|
||||
|
||||
KeyEventResult _handleKeyEvent(FocusNode _, KeyEvent event) {
|
||||
final key = event.logicalKey;
|
||||
|
||||
if (key.isBackKey) {
|
||||
if (event is KeyDownEvent) {
|
||||
_backKeyDownSeen = true;
|
||||
} else if (event is KeyUpEvent && !_backKeyDownSeen) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (event is KeyUpEvent) {
|
||||
_backKeyDownSeen = false;
|
||||
}
|
||||
}
|
||||
|
||||
final backResult = handleBackKeyAction(event, () {
|
||||
if (_movingIndex != null) {
|
||||
setState(() {
|
||||
if (_originalOrder != null) {
|
||||
_tempFavorites = List.from(_originalOrder!);
|
||||
}
|
||||
_focusedIndex = _originalIndex ?? 0;
|
||||
_movingIndex = null;
|
||||
_originalIndex = null;
|
||||
_originalOrder = null;
|
||||
});
|
||||
} else {
|
||||
OverlaySheetController.popAdaptive(context);
|
||||
}
|
||||
});
|
||||
if (backResult != KeyEventResult.ignored) {
|
||||
return backResult;
|
||||
}
|
||||
|
||||
if (!event.isActionable) return KeyEventResult.ignored;
|
||||
|
||||
if (_movingIndex != null) {
|
||||
if (key.isUpKey && _movingIndex! > 0) {
|
||||
setState(() {
|
||||
final item = _tempFavorites.removeAt(_movingIndex!);
|
||||
_tempFavorites.insert(_movingIndex! - 1, item);
|
||||
_movingIndex = _movingIndex! - 1;
|
||||
_focusedIndex = _movingIndex!;
|
||||
});
|
||||
_ensureFocusedVisible();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key.isDownKey && _movingIndex! < _tempFavorites.length - 1) {
|
||||
setState(() {
|
||||
final item = _tempFavorites.removeAt(_movingIndex!);
|
||||
_tempFavorites.insert(_movingIndex! + 1, item);
|
||||
_movingIndex = _movingIndex! + 1;
|
||||
_focusedIndex = _movingIndex!;
|
||||
});
|
||||
_ensureFocusedVisible();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key.isSelectKey) {
|
||||
widget.onReorder(_tempFavorites);
|
||||
setState(() {
|
||||
_movingIndex = null;
|
||||
_originalIndex = null;
|
||||
_originalOrder = null;
|
||||
});
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
} else {
|
||||
if (key.isUpKey && _focusedIndex > 0) {
|
||||
setState(() {
|
||||
_focusedIndex--;
|
||||
_focusedColumn = 0;
|
||||
});
|
||||
_ensureFocusedVisible();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key.isDownKey && _focusedIndex < _tempFavorites.length - 1) {
|
||||
setState(() {
|
||||
_focusedIndex++;
|
||||
_focusedColumn = 0;
|
||||
});
|
||||
_ensureFocusedVisible();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key.isLeftKey && _focusedColumn > 0) {
|
||||
setState(() => _focusedColumn--);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key.isRightKey && _focusedColumn < 1) {
|
||||
setState(() => _focusedColumn++);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key.isSelectKey) {
|
||||
if (_focusedColumn == 0) {
|
||||
setState(() {
|
||||
_movingIndex = _focusedIndex;
|
||||
_originalIndex = _focusedIndex;
|
||||
_originalOrder = List.from(_tempFavorites);
|
||||
});
|
||||
} else if (_focusedColumn == 1) {
|
||||
_removeItem(_focusedIndex);
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
|
||||
if (key.isDpadDirection) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
void _onReorder(int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final item = _tempFavorites.removeAt(oldIndex);
|
||||
_tempFavorites.insert(newIndex, item);
|
||||
});
|
||||
widget.onReorder(_tempFavorites);
|
||||
}
|
||||
|
||||
void _removeItem(int index) {
|
||||
final removed = _tempFavorites[index];
|
||||
setState(() {
|
||||
_tempFavorites.removeAt(index);
|
||||
if (_focusedIndex >= _tempFavorites.length) {
|
||||
_focusedIndex = (_tempFavorites.length - 1).clamp(0, _tempFavorites.length);
|
||||
}
|
||||
});
|
||||
widget.onRemove(removed);
|
||||
|
||||
if (_tempFavorites.isEmpty) {
|
||||
OverlaySheetController.popAdaptive(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isKeyboardMode = InputModeTracker.isKeyboardMode(context);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BottomSheetHeader(
|
||||
title: t.liveTv.reorderFavorites,
|
||||
icon: Symbols.swap_vert_rounded,
|
||||
),
|
||||
Expanded(
|
||||
child: Focus(
|
||||
focusNode: _listFocusNode,
|
||||
autofocus: isKeyboardMode,
|
||||
onKeyEvent: _handleKeyEvent,
|
||||
child: ReorderableListView.builder(
|
||||
scrollController: _scrollController,
|
||||
onReorder: _onReorder,
|
||||
itemCount: _tempFavorites.length,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (context, index) {
|
||||
final fav = _tempFavorites[index];
|
||||
final channel = widget.channelMap[fav.id];
|
||||
final isFocused = isKeyboardMode && index == _focusedIndex;
|
||||
final isMoving = index == _movingIndex;
|
||||
|
||||
return _buildFavoriteTile(
|
||||
key: ValueKey(fav.id),
|
||||
fav: fav,
|
||||
channel: channel,
|
||||
index: index,
|
||||
isFocused: isFocused,
|
||||
isMoving: isMoving,
|
||||
focusedColumn: isFocused ? _focusedColumn : null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFavoriteTile({
|
||||
required Key key,
|
||||
required FavoriteChannel fav,
|
||||
required LiveTvChannel? channel,
|
||||
required int index,
|
||||
required bool isFocused,
|
||||
required bool isMoving,
|
||||
int? focusedColumn,
|
||||
}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final multiServer = context.read<MultiServerProvider>();
|
||||
final client = multiServer.getClientForServer(channel?.serverId ?? '');
|
||||
|
||||
Color? tileColor;
|
||||
if (isMoving) {
|
||||
tileColor = colorScheme.primaryContainer;
|
||||
} else if (isFocused && focusedColumn == 0) {
|
||||
tileColor = colorScheme.surfaceContainerHighest;
|
||||
}
|
||||
|
||||
final isRemoveButtonFocused = isFocused && focusedColumn == 1;
|
||||
final displayName = channel?.displayName ?? fav.title ?? fav.id;
|
||||
final channelNumber = channel?.number ?? fav.vcn;
|
||||
|
||||
return Container(
|
||||
key: key,
|
||||
decoration: BoxDecoration(color: tileColor),
|
||||
child: ListTile(
|
||||
leading: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ReorderableDragStartListener(
|
||||
index: index,
|
||||
child: AppIcon(
|
||||
isMoving ? Symbols.swap_vert_rounded : Symbols.drag_indicator_rounded,
|
||||
fill: 1,
|
||||
color: isMoving ? colorScheme.primary : IconTheme.of(context).color?.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: channel?.thumb != null && client != null
|
||||
? PlexOptimizedImage.thumb(
|
||||
client: client,
|
||||
imagePath: channel!.thumb,
|
||||
width: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
: Center(
|
||||
child: AppIcon(Symbols.live_tv_rounded, fill: 1, color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Text(displayName, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: channelNumber != null
|
||||
? Text(
|
||||
channelNumber,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color?.withValues(alpha: 0.6),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
trailing: Container(
|
||||
decoration: FocusTheme.focusBackgroundDecoration(
|
||||
isFocused: isRemoveButtonFocused,
|
||||
borderRadius: 20,
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const AppIcon(Symbols.close_rounded, fill: 1, size: 20),
|
||||
onPressed: () => _removeItem(index),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user