Skip to content

Commit 1927bfa

Browse files
authored
fix: make all podcast pages fully ID based (#1359)
* fix: make all podcast pages fully ID based * upgrade flutter and packages
1 parent d90af5b commit 1927bfa

File tree

72 files changed

+1743
-610
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+1743
-610
lines changed

.fvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"flutter": "3.35.3"
2+
"flutter": "3.35.4"
33
}

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,6 @@ jobs:
3535
- uses: actions/checkout@v5
3636
- uses: kuhnroyal/flutter-fvm-config-action/setup@v3
3737
- run: sudo apt update
38-
- run: sudo apt install -y clang cmake curl libgtk-3-dev ninja-build pkg-config unzip libunwind-dev libmpv-dev
38+
- run: sudo apt install -y clang cmake curl libgtk-3-dev ninja-build pkg-config unzip libunwind-dev libmpv-dev libnotify-dev
3939
- run: flutter pub get
4040
- run: flutter build linux -v

.gitignore

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,7 @@ app.*.map.json
4646
/android/app/release
4747
/android/app/.cxx/
4848

49-
.vscode/
50-
musicpod_0.1.0_amd64.snap
51-
#pubspec.lock
52-
pinnedAlbums.json
53-
playlists.json
54-
podcasts.json
55-
starredStations.json
49+
5650

5751
# FVM Version Cache
5852
.fvm/

.vscode/launch.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "musicpod",
9+
"request": "launch",
10+
"type": "dart"
11+
},
12+
{
13+
"name": "musicpod (profile mode)",
14+
"request": "launch",
15+
"type": "dart",
16+
"flutterMode": "profile"
17+
},
18+
{
19+
"name": "musicpod (release mode)",
20+
"request": "launch",
21+
"type": "dart",
22+
"flutterMode": "release"
23+
}
24+
]
25+
}

.vscode/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"yaml.schemaStore.enable": false,
3+
"dart.flutterSdkPath": ".fvm/versions/3.35.4"
4+
}

l10n.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@ arb-dir: lib/l10n
22
template-arb-file: app_en.arb
33
output-localization-file: app_localizations.dart
44
nullable-getter: false
5-
synthetic-package: false
65
format: true

lib/app/view/master_tile.dart

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/material.dart';
2+
import 'package:future_loading_dialog/future_loading_dialog.dart';
23
import 'package:watch_it/watch_it.dart';
34
import 'package:yaru/yaru.dart';
45

@@ -11,9 +12,11 @@ import '../../common/view/spaced_divider.dart';
1112
import '../../common/view/theme.dart';
1213
import '../../common/view/ui_constants.dart';
1314
import '../../extensions/build_context_x.dart';
15+
import '../../l10n/l10n.dart';
1416
import '../../library/library_model.dart';
1517
import '../../local_audio/local_audio_model.dart';
1618
import '../../player/player_model.dart';
19+
import '../../podcasts/podcast_model.dart';
1720
import '../../radio/radio_model.dart';
1821
import 'master_item.dart';
1922
import 'routing_manager.dart';
@@ -160,7 +163,14 @@ class __PlayAbleMasterTileState extends State<_PlayAbleMasterTile> {
160163
child: IconButton(
161164
style: tonedIconButtonStyle(context.colorScheme),
162165
onPressed: () async {
163-
final audios = await getAudiosById(widget.pageId);
166+
final result = await showFutureLoadingDialog(
167+
context: context,
168+
future: () async => await getAudiosById(widget.pageId),
169+
backLabel: context.l10n.back,
170+
barrierDismissible: true,
171+
title: context.l10n.loadingPleaseWait,
172+
);
173+
final audios = result.asValue?.value;
164174
if (audios?.firstOrNull?.audioType == AudioType.radio) {
165175
di<RadioModel>().clickStation(audios?.firstOrNull);
166176
}
@@ -200,8 +210,17 @@ class __PlayAbleMasterTileState extends State<_PlayAbleMasterTile> {
200210
return audio == null ? [] : [audio];
201211
}
202212

203-
return libraryModel.getPodcast(pageId) ??
204-
libraryModel.getPlaylistById(pageId) ??
205-
(await di<LocalAudioModel>().findAlbum(pageId));
213+
if (libraryModel.isPodcastSubscribed(pageId)) {
214+
final episodes =
215+
di<PodcastModel>().getPodcastEpisodesFromCache(pageId) ??
216+
await di<PodcastModel>().findEpisodes(feedUrl: pageId);
217+
return episodes;
218+
}
219+
220+
if (libraryModel.isPlaylistSaved(pageId)) {
221+
return libraryModel.getPlaylistById(pageId);
222+
}
223+
224+
return di<LocalAudioModel>().findAlbum(pageId);
206225
}
207226
}

lib/common/view/icons.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,10 @@ class Iconz {
256256
? CupertinoIcons.plus
257257
: Icons.add;
258258
static IconData get removeFromLibrary => yaru
259-
? YaruIcons.checkmark
259+
? YaruIcons.minus
260260
: cupertino
261-
? CupertinoIcons.check_mark
262-
: Icons.check;
261+
? CupertinoIcons.minus
262+
: Icons.remove;
263263
static IconData get refresh => yaru
264264
? YaruIcons.refresh
265265
: cupertino

lib/custom_content/custom_content_model.dart

Lines changed: 68 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import 'dart:io';
22

3-
import 'package:collection/collection.dart';
4-
import 'package:file_selector/file_selector.dart';
53
import 'package:flutter/foundation.dart';
64
import 'package:m3u_parser_nullsafe/m3u_parser_nullsafe.dart';
75
import 'package:opml/opml.dart';
@@ -36,8 +34,8 @@ class CustomContentModel extends SafeChangeNotifier {
3634

3735
List<({List<Audio> audios, String id})> _playlists = [];
3836
List<({List<Audio> audios, String id})> get playlists => _playlists;
39-
void setPlaylists(List<({List<Audio> audios, String id})> value) {
40-
_playlists = value;
37+
Future<void> addPlaylists() async {
38+
_playlists = [..._playlists, ...await loadPlaylists()];
4139
notifyListeners();
4240
}
4341

@@ -46,18 +44,11 @@ class CustomContentModel extends SafeChangeNotifier {
4644
notifyListeners();
4745
}
4846

49-
Future<void> addPlaylists({List<XFile>? files}) async =>
50-
setPlaylists([..._playlists, ...await loadPlaylists(files: files)]);
51-
52-
Future<List<({List<Audio> audios, String id})>> loadPlaylists({
53-
List<XFile>? files,
54-
}) async {
47+
Future<List<({List<Audio> audios, String id})>> loadPlaylists() async {
5548
final List<({List<Audio> audios, String id})> lists = [];
5649

5750
try {
58-
final paths =
59-
files?.map((e) => e.path) ??
60-
await _externalPathService.getPathsOfFiles();
51+
final paths = await _externalPathService.getPathsOfFiles();
6152
for (var path in paths) {
6253
if (path.endsWith('.m3u')) {
6354
lists.add((
@@ -131,52 +122,77 @@ class CustomContentModel extends SafeChangeNotifier {
131122
File(join(basePath, '$id.m3u')).writeAsStringSync(m3uAsString.toString());
132123
}
133124

134-
bool _processing = false;
135-
bool get processing => _processing;
136-
void setProcessing(bool value) {
137-
if (_processing == value) return;
138-
_processing = value;
139-
notifyListeners();
140-
}
141-
142125
Future<void> importPodcastsFromOpmlFile() async {
143-
if (_processing) return;
144-
setProcessing(true);
126+
final podcasts =
127+
<({String artist, String feedUrl, String? imageUrl, String name})>[];
128+
145129
final path = await _externalPathService.getPathOfFile();
146130

147131
if (path == null) {
148-
setProcessing(false);
149132
return;
150133
}
151134
final file = File(path);
152135
if (!file.existsSync()) return;
153136
final xml = file.readAsStringSync();
154137
final doc = OpmlDocument.parse(xml);
155138

156-
for (var category in doc.body.where((e) => e.children != null)) {
157-
final children = category.children!.where((e) => e.xmlUrl != null);
158-
final podcasts = <(String feedUrl, List<Audio> audios)>[];
159-
for (var feed in children) {
160-
final audios = await _podcastService.findEpisodes(
161-
feedUrl: feed.xmlUrl!,
139+
for (var outline in doc.body) {
140+
if (outline.xmlUrl != null) {
141+
final maybe = await _findPodcast(
142+
outline.xmlUrl!,
143+
text: outline.text,
144+
title: outline.title,
162145
);
163-
if (audios.isNotEmpty) {
164-
podcasts.add((feed.xmlUrl!, audios));
146+
if (maybe != null) {
147+
podcasts.add(maybe);
148+
}
149+
} else {
150+
for (var outlineChild in (outline.children ?? <OpmlOutline>[]).where(
151+
(e) => e.xmlUrl != null,
152+
)) {
153+
final maybe = await _findPodcast(
154+
outlineChild.xmlUrl!,
155+
text: outlineChild.text,
156+
title: outlineChild.title,
157+
);
158+
if (maybe != null) {
159+
podcasts.add(maybe);
160+
}
165161
}
166162
}
167-
if (podcasts.isNotEmpty) {
168-
_libraryService.addPodcasts(podcasts);
169-
}
170163
}
171-
setProcessing(false);
164+
165+
if (podcasts.isNotEmpty) {
166+
await _libraryService.addPodcasts(podcasts);
167+
}
168+
}
169+
170+
Future<({String artist, String feedUrl, String? imageUrl, String name})?>
171+
_findPodcast(String feed, {String? text, String? title}) async {
172+
if (title != null && text != null) {
173+
return (feedUrl: feed, artist: text, imageUrl: null, name: title);
174+
}
175+
176+
// Only load the feed if the fields are not provided because this is expensive
177+
final audios = await _podcastService.findEpisodes(feedUrl: feed);
178+
final artist = audios.first.artist ?? '';
179+
final imageUrl = audios.first.albumArtUrl ?? audios.first.imageUrl;
180+
final name = audios.first.album ?? '';
181+
if (audios.isNotEmpty) {
182+
final value = (
183+
feedUrl: feed,
184+
artist: artist,
185+
imageUrl: imageUrl,
186+
name: name,
187+
);
188+
return value;
189+
}
190+
return null;
172191
}
173192

174193
Future<bool> exportPodcastsToOpmlFile() async {
175-
if (_processing) return false;
176-
setProcessing(true);
177194
final location = await _externalPathService.getPathOfDirectory();
178195
if (location == null) {
179-
setProcessing(false);
180196
return false;
181197
}
182198

@@ -188,32 +204,31 @@ class CustomContentModel extends SafeChangeNotifier {
188204
final body = <OpmlOutline>[];
189205
final category = OpmlOutlineBuilder();
190206

191-
for (var podcast in _libraryService.podcasts.entries) {
192-
category.addChild(
193-
OpmlOutlineBuilder()
194-
.type('rss')
195-
.title(podcast.value.firstOrNull?.album ?? '')
196-
.text(podcast.value.firstOrNull?.artist ?? '')
197-
.xmlUrl(podcast.key)
198-
.build(),
199-
);
207+
for (var podcast in _libraryService.podcasts) {
208+
final name = _libraryService.getSubscribedPodcastName(podcast);
209+
final artist = _libraryService.getSubscribedPodcastArtist(podcast);
210+
final builder = OpmlOutlineBuilder().type('rss').xmlUrl(podcast);
211+
if (name != null) {
212+
builder.title(name);
213+
}
214+
if (artist != null) {
215+
builder.text(artist);
216+
}
217+
category.addChild(builder.build());
200218
}
201219

202220
body.add(category.type('rss').title('Podcasts').text('Podcasts').build());
203221

204222
final opml = OpmlDocument(head: head, body: body);
205223
final xml = opml.toXmlString(pretty: true);
206224
file.writeAsStringSync(xml);
207-
setProcessing(false);
225+
208226
return true;
209227
}
210228

211229
Future<bool> exportStarredStationsToOpmlFile() async {
212-
if (_processing) return false;
213-
setProcessing(true);
214230
final location = await _externalPathService.getPathOfDirectory();
215231
if (location == null) {
216-
setProcessing(false);
217232
return false;
218233
}
219234

@@ -236,17 +251,14 @@ class CustomContentModel extends SafeChangeNotifier {
236251
final opml = OpmlDocument(head: head, body: body);
237252
final xml = opml.toXmlString(pretty: true);
238253
file.writeAsStringSync(xml);
239-
setProcessing(false);
254+
240255
return true;
241256
}
242257

243258
Future<void> importStarredStationsFromOpmlFile() async {
244-
if (_processing) return;
245-
setProcessing(true);
246259
final path = await _externalPathService.getPathOfFile();
247260

248261
if (path == null) {
249-
setProcessing(false);
250262
return;
251263
}
252264
final file = File(path);
@@ -264,7 +276,6 @@ class CustomContentModel extends SafeChangeNotifier {
264276
_libraryService.addStarredStations(starredStations);
265277
}
266278
}
267-
setProcessing(false);
268279
}
269280

270281
String? _playlistName;
@@ -278,7 +289,6 @@ class CustomContentModel extends SafeChangeNotifier {
278289
void reset() {
279290
_playlists = [];
280291
_playlistName = null;
281-
_processing = false;
282292
notifyListeners();
283293
}
284294
}

lib/custom_content/view/custom_playlists_section.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:collection/collection.dart';
22
import 'package:flutter/material.dart';
3+
import 'package:future_loading_dialog/future_loading_dialog.dart';
34
import 'package:watch_it/watch_it.dart';
45
import 'package:yaru/yaru.dart';
56

@@ -90,9 +91,12 @@ class CustomPlaylistsSection extends StatelessWidget with WatchItMixin {
9091
],
9192
),
9293
TextButton(
93-
onPressed: () {
94-
di<CustomContentModel>().addPlaylists();
95-
},
94+
onPressed: () => showFutureLoadingDialog(
95+
context: context,
96+
future: () => di<CustomContentModel>().addPlaylists(),
97+
backLabel: context.l10n.back,
98+
title: context.l10n.importingPlaylistsPleaseWait,
99+
),
96100
child: Text(l10n.loadFromFileOptional),
97101
),
98102
...playlists.map((e) {

0 commit comments

Comments
 (0)