11 Commits

Author SHA1 Message Date
austinried
7f83204b24 don't pass all ids as params
instead, only pass ids to delete and chunk those by the param limit
2023-05-07 13:56:05 +09:00
austinried
0fe52494d0 update todo 2023-05-07 13:54:56 +09:00
Daniel Playfair Cal
56dbcde3b4 add autofill hints for source page 2023-05-07 13:54:26 +09:00
austinried
8fbc5e6ce4 add artist radio 2023-05-07 13:28:15 +09:00
austinried
979a4b7c73 add plaintext password option
fixes #161
2023-05-06 17:56:03 +09:00
josé m
7b1da24748 Translated using Weblate (Galician)
Currently translated at 100.0% (92 of 92 strings)

Co-authored-by: josé m <correoxm@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/gl/
Translation: Subtracks/subtracks
2023-05-06 09:11:25 +09:00
Sacelo
7014aa85d1 Translated using Weblate (Spanish)
Currently translated at 77.1% (71 of 92 strings)

Co-authored-by: Sacelo <rion020806@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/es/
Translation: Subtracks/subtracks
2023-05-06 09:11:25 +09:00
雨杉叶
abab674322 Translated using Weblate (Chinese (Simplified))
Currently translated at 88.0% (81 of 92 strings)

Co-authored-by: 雨杉叶 <yushaye@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/zh_Hans/
Translation: Subtracks/subtracks
2023-05-06 09:11:25 +09:00
daniel-225
498bb22a69 Translated using Weblate (German)
Currently translated at 75.0% (69 of 92 strings)

Co-authored-by: daniel-225 <dsoukup@outlook.de>
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/de/
Translation: Subtracks/subtracks
2023-05-06 09:11:25 +09:00
austinried
11fe43a750 add server info fields 2023-04-28 21:59:33 +09:00
austinried
2a60a7306c use english as last fallback locale
fixes #160
2023-04-28 21:45:59 +09:00
20 changed files with 1164 additions and 946 deletions

View File

@@ -23,10 +23,14 @@ A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Smartphone (please complete the following information):** **Device**
- Device: [e.g. Pixel 4] - Model: [e.g. Pixel 4]
- OS: [e.g. Android 12] - OS: [e.g. Android 12]
- Subtracks version [e.g. 1.2.0] - Subtracks version [e.g. 1.2.0]
**Server**
- Software: [e.g. Navidrome]
- Version: [e.g. 0.49.3]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View File

@@ -146,13 +146,8 @@
], ],
"de": [ "de": [
"actionsCancel",
"actionsDelete",
"actionsDownload",
"actionsDownloadCancel",
"actionsDownloadDelete", "actionsDownloadDelete",
"actionsOk", "actionsOk",
"controlsShuffle",
"resourcesAlbumCount", "resourcesAlbumCount",
"resourcesArtistCount", "resourcesArtistCount",
"resourcesFilterAlbum", "resourcesFilterAlbum",
@@ -177,13 +172,6 @@
], ],
"es": [ "es": [
"actionsCancel",
"actionsDelete",
"actionsDownload",
"actionsDownloadCancel",
"actionsDownloadDelete",
"actionsOk",
"controlsShuffle",
"resourcesAlbumCount", "resourcesAlbumCount",
"resourcesArtistCount", "resourcesArtistCount",
"resourcesFilterAlbum", "resourcesFilterAlbum",
@@ -238,37 +226,6 @@
"settingsServersFieldsName" "settingsServersFieldsName"
], ],
"gl": [
"actionsCancel",
"actionsDelete",
"actionsDownload",
"actionsDownloadCancel",
"actionsDownloadDelete",
"actionsOk",
"controlsShuffle",
"resourcesAlbumCount",
"resourcesArtistCount",
"resourcesFilterAlbum",
"resourcesFilterArtist",
"resourcesFilterOwner",
"resourcesFilterYear",
"resourcesPlaylistCount",
"resourcesSongCount",
"resourcesSongListDeleteAllContent",
"resourcesSongListDeleteAllTitle",
"resourcesSortByAlbum",
"resourcesSortByAlbumCount",
"resourcesSortByTitle",
"resourcesSortByUpdated",
"settingsAboutActionsSupport",
"settingsNetworkOptionsOfflineMode",
"settingsNetworkOptionsOfflineModeOff",
"settingsNetworkOptionsOfflineModeOn",
"settingsNetworkOptionsStreamFormat",
"settingsNetworkOptionsStreamFormatServerDefault",
"settingsServersFieldsName"
],
"it": [ "it": [
"actionsCancel", "actionsCancel",
"actionsDelete", "actionsDelete",
@@ -594,28 +551,11 @@
], ],
"zh": [ "zh": [
"actionsCancel",
"actionsDelete",
"actionsDownload",
"actionsDownloadCancel",
"actionsDownloadDelete",
"actionsOk",
"controlsShuffle", "controlsShuffle",
"resourcesAlbumCount", "resourcesAlbumCount",
"resourcesArtistCount", "resourcesArtistCount",
"resourcesFilterAlbum",
"resourcesFilterArtist",
"resourcesFilterOwner",
"resourcesFilterYear",
"resourcesPlaylistCount", "resourcesPlaylistCount",
"resourcesSongCount", "resourcesSongCount",
"resourcesSongListDeleteAllContent",
"resourcesSongListDeleteAllTitle",
"resourcesSortByAlbum",
"resourcesSortByAlbumCount",
"resourcesSortByTitle",
"resourcesSortByUpdated",
"settingsAboutActionsSupport",
"settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineMode",
"settingsNetworkOptionsOfflineModeOff", "settingsNetworkOptionsOfflineModeOff",
"settingsNetworkOptionsOfflineModeOn", "settingsNetworkOptionsOfflineModeOn",

View File

@@ -14,14 +14,10 @@
- [ ] Library list display modes - [ ] Library list display modes
- [ ] Search - [ ] Search
- [ ] Individual "more" results pages - [ ] Individual "more" results pages
- [ ] Radio modes
- [ ] Artist
- [ ] Now playing gestures - [ ] Now playing gestures
- [ ] Swipe bar/album to skip - [ ] Swipe bar/album to skip
- [ ] Double-tap to seek forward/back (bar only) - [ ] Double-tap to seek forward/back (bar only)
- [ ] Settings - [ ] Settings
- [ ] Sources
- [ ] Use plaintext password
- [ ] Music - [ ] Music
- [ ] Scrobble - [ ] Scrobble
- [ ] Downloads - [ ] Downloads

View File

@@ -1,3 +1,4 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -77,7 +78,8 @@ class App extends HookConsumerWidget {
), ),
routeInformationParser: appRouter.defaultRouteParser(), routeInformationParser: appRouter.defaultRouteParser(),
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: [...AppLocalizations.supportedLocales]
..moveToTheFront(const Locale('en')),
); );
} }
} }

View File

@@ -1,13 +1,19 @@
import 'dart:math'; import 'dart:math';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../database/database.dart';
import '../../models/query.dart';
import '../../models/support.dart';
import '../../services/audio_service.dart';
import '../../state/music.dart'; import '../../state/music.dart';
import '../../state/settings.dart'; import '../../state/settings.dart';
import '../app_router.dart'; import '../app_router.dart';
import '../buttons.dart';
import '../images.dart'; import '../images.dart';
import '../items.dart'; import '../items.dart';
@@ -27,6 +33,26 @@ class ArtistPage extends HookConsumerWidget {
final albums = ref.watch(albumsByArtistIdProvider(id)); final albums = ref.watch(albumsByArtistIdProvider(id));
return Scaffold( return Scaffold(
floatingActionButton: RadioPlayFab(
onPressed: () => artist.hasValue
? ref.read(audioControlProvider).playRadio(
context: QueueContextType.artist,
contextId: artist.valueOrNull!.id,
query: ListQuery(
filters: IList([
FilterWith.equals(
column: 'artist_id',
value: artist.valueOrNull!.id,
)
]),
),
getSongs: (query) => ref
.read(databaseProvider)
.songsList(ref.read(sourceIdProvider), query)
.get(),
)
: null,
),
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(

View File

@@ -41,6 +41,7 @@ class SourcePage extends HookConsumerWidget {
label: l.settingsServersFieldsAddress, label: l.settingsServersFieldsAddress,
initialValue: source?.address.toString(), initialValue: source?.address.toString(),
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
autofillHints: const [AutofillHints.url],
required: true, required: true,
validator: (value, label) { validator: (value, label) {
if (Uri.tryParse(value!) == null) { if (Uri.tryParse(value!) == null) {
@@ -52,15 +53,27 @@ class SourcePage extends HookConsumerWidget {
final username = LabeledTextField( final username = LabeledTextField(
label: l.settingsServersFieldsUsername, label: l.settingsServersFieldsUsername,
initialValue: source?.username, initialValue: source?.username,
autofillHints: const [AutofillHints.username],
required: true, required: true,
); );
final password = LabeledTextField( final password = LabeledTextField(
label: l.settingsServersFieldsPassword, label: l.settingsServersFieldsPassword,
initialValue: source?.password, initialValue: source?.password,
obscureText: true, obscureText: true,
autofillHints: const [AutofillHints.password],
required: true, required: true,
); );
final forcePlaintextPassword = useState(!(source?.useTokenAuth ?? false));
final forcePlaintextSwitch = SwitchListTile(
value: forcePlaintextPassword.value,
title: Text(l.settingsServersOptionsForcePlaintextPasswordTitle),
subtitle: forcePlaintextPassword.value
? Text(l.settingsServersOptionsForcePlaintextPasswordDescriptionOn)
: Text(l.settingsServersOptionsForcePlaintextPasswordDescriptionOff),
onChanged: (value) => forcePlaintextPassword.value = value,
);
return WillPopScope( return WillPopScope(
onWillPop: () async => !isSaving.value && !isDeleting.value, onWillPop: () async => !isSaving.value && !isDeleting.value,
child: Scaffold( child: Scaffold(
@@ -128,6 +141,7 @@ class SourcePage extends HookConsumerWidget {
address: Uri.parse(address.value), address: Uri.parse(address.value),
username: username.value, username: username.value,
password: password.value, password: password.value,
useTokenAuth: !forcePlaintextPassword.value,
), ),
); );
} else { } else {
@@ -142,7 +156,8 @@ class SourcePage extends HookConsumerWidget {
features: IList(), features: IList(),
username: username.value, username: username.value,
password: password.value, password: password.value,
useTokenAuth: const Value(true), useTokenAuth:
Value(!forcePlaintextPassword.value),
), ),
); );
} }
@@ -163,21 +178,25 @@ class SourcePage extends HookConsumerWidget {
), ),
body: Form( body: Form(
key: form, key: form,
child: Padding( child: AutofillGroup(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: ListView( child: ListView(
children: [ children: [
const SizedBox(height: 96 - kToolbarHeight), const SizedBox(height: 96 - kToolbarHeight),
Text( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
source == null source == null
? l.settingsServersActionsAdd ? l.settingsServersActionsAdd
: l.settingsServersActionsEdit, : l.settingsServersActionsEdit,
style: theme.textTheme.displaySmall, style: theme.textTheme.displaySmall,
), ),
),
name, name,
address, address,
username, username,
password, password,
const SizedBox(height: 24),
forcePlaintextSwitch,
const FabPadding(), const FabPadding(),
], ],
), ),
@@ -194,6 +213,7 @@ class LabeledTextField extends HookConsumerWidget {
final bool obscureText; final bool obscureText;
final bool required; final bool required;
final TextInputType? keyboardType; final TextInputType? keyboardType;
final Iterable<String>? autofillHints;
final String? Function(String? value, String label)? validator; final String? Function(String? value, String label)? validator;
// ignore: prefer_const_constructors_in_immutables // ignore: prefer_const_constructors_in_immutables
@@ -204,6 +224,7 @@ class LabeledTextField extends HookConsumerWidget {
this.obscureText = false, this.obscureText = false,
this.keyboardType, this.keyboardType,
this.validator, this.validator,
this.autofillHints,
this.required = false, this.required = false,
}); });
@@ -224,18 +245,18 @@ class LabeledTextField extends HookConsumerWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
_controller = useTextEditingController(text: initialValue); _controller = useTextEditingController(text: initialValue);
return Column( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(label, style: theme.textTheme.titleMedium),
label,
style: theme.textTheme.titleMedium,
),
TextFormField( TextFormField(
controller: _controller, controller: _controller,
obscureText: obscureText, obscureText: obscureText,
keyboardType: keyboardType, keyboardType: keyboardType,
autofillHints: autofillHints,
validator: (value) { validator: (value) {
String? error; String? error;
@@ -253,6 +274,7 @@ class LabeledTextField extends HookConsumerWidget {
}, },
), ),
], ],
),
); );
} }
} }

View File

@@ -17,6 +17,10 @@ import 'converters.dart';
part 'database.g.dart'; part 'database.g.dart';
// don't exceed SQLITE_MAX_VARIABLE_NUMBER (32766 for version >= 3.32.0)
// https://www.sqlite.org/limits.html
const kSqliteMaxVariableNumber = 32766;
@DriftDatabase(include: {'tables.drift'}) @DriftDatabase(include: {'tables.drift'})
class SubtracksDatabase extends _$SubtracksDatabase { class SubtracksDatabase extends _$SubtracksDatabase {
SubtracksDatabase() : super(_openConnection()); SubtracksDatabase() : super(_openConnection());
@@ -169,13 +173,28 @@ class SubtracksDatabase extends _$SubtracksDatabase {
}); });
} }
Future<void> deleteArtistsNotIn(int sourceId, Iterable<String> ids) async { Future<void> deleteArtistsNotIn(int sourceId, Set<String> ids) {
return transaction(() async {
final allIds = (await (selectOnly(artists)
..addColumns([artists.id])
..where(artists.sourceId.equals(sourceId)))
.map((row) => row.read(artists.id))
.get())
.whereNotNull()
.toSet();
final downloadIds = (await artistIdsWithDownloadStatus(sourceId).get())
.whereNotNull()
.toSet();
final diff = allIds.difference(downloadIds).difference(ids);
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
await (delete(artists) await (delete(artists)
..where( ..where(
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isNotIn(ids), (tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
))
.go(); .go();
} }
});
}
Future<void> saveAlbums(Iterable<AlbumsCompanion> albums) async { Future<void> saveAlbums(Iterable<AlbumsCompanion> albums) async {
await batch((batch) { await batch((batch) {
@@ -183,16 +202,28 @@ class SubtracksDatabase extends _$SubtracksDatabase {
}); });
} }
Future<void> deleteAlbumsNotIn(int sourceId, Iterable<String> ids) async { Future<void> deleteAlbumsNotIn(int sourceId, Set<String> ids) {
final alsoKeep = (await albumIdsWithDownloaded(sourceId).get()).toSet(); return transaction(() async {
final allIds = (await (selectOnly(albums)
..addColumns([albums.id])
..where(albums.sourceId.equals(sourceId)))
.map((row) => row.read(albums.id))
.get())
.whereNotNull()
.toSet();
final downloadIds = (await albumIdsWithDownloadStatus(sourceId).get())
.whereNotNull()
.toSet();
ids = ids.toList()..addAll(alsoKeep); final diff = allIds.difference(downloadIds).difference(ids);
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
await (delete(albums) await (delete(albums)
..where( ..where(
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isNotIn(ids), (tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
))
.go(); .go();
} }
});
}
Future<void> savePlaylists( Future<void> savePlaylists(
Iterable<PlaylistWithSongsCompanion> playlistsWithSongs, Iterable<PlaylistWithSongsCompanion> playlistsWithSongs,
@@ -215,19 +246,32 @@ class SubtracksDatabase extends _$SubtracksDatabase {
}); });
} }
Future<void> deletePlaylistsNotIn(int sourceId, Iterable<String> ids) async { Future<void> deletePlaylistsNotIn(int sourceId, Set<String> ids) {
return transaction(() async {
final allIds = (await (selectOnly(playlists)
..addColumns([playlists.id])
..where(playlists.sourceId.equals(sourceId)))
.map((row) => row.read(playlists.id))
.get())
.whereNotNull()
.toSet();
final downloadIds = (await playlistIdsWithDownloadStatus(sourceId).get())
.whereNotNull()
.toSet();
final diff = allIds.difference(downloadIds).difference(ids);
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
await (delete(playlists) await (delete(playlists)
..where( ..where(
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isNotIn(ids), (tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
))
.go(); .go();
await (delete(playlistSongs) await (delete(playlistSongs)
..where( ..where((tbl) =>
(tbl) => tbl.sourceId.equals(sourceId) & tbl.playlistId.isIn(slice)))
tbl.sourceId.equals(sourceId) & tbl.playlistId.isNotIn(ids),
))
.go(); .go();
} }
});
}
Future<void> savePlaylistSongs( Future<void> savePlaylistSongs(
int sourceId, int sourceId,
@@ -250,30 +294,34 @@ class SubtracksDatabase extends _$SubtracksDatabase {
}); });
} }
Future<void> deleteSongsNotIn(int sourceId, Iterable<String> ids) async { Future<void> deleteSongsNotIn(int sourceId, Set<String> ids) {
await (delete(songs) return transaction(() async {
..where( final allIds = (await (selectOnly(songs)
(tbl) =>
tbl.sourceId.equals(sourceId) &
tbl.id.isNotIn(ids) &
tbl.downloadFilePath.isNull() &
tbl.downloadTaskId.isNull(),
))
.go();
final remainingIds = (await (selectOnly(songs)
..addColumns([songs.id]) ..addColumns([songs.id])
..where(songs.sourceId.equals(sourceId))) ..where(
songs.sourceId.equals(sourceId) &
songs.downloadFilePath.isNull() &
songs.downloadTaskId.isNull(),
))
.map((row) => row.read(songs.id)) .map((row) => row.read(songs.id))
.get()) .get())
.whereNotNull(); .whereNotNull()
.toSet();
final diff = allIds.difference(ids);
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
await (delete(songs)
..where(
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
.go();
await (delete(playlistSongs) await (delete(playlistSongs)
..where( ..where(
(tbl) => (tbl) => tbl.sourceId.equals(sourceId) & tbl.songId.isIn(slice),
tbl.sourceId.equals(sourceId) &
tbl.songId.isNotIn(remainingIds),
)) ))
.go(); .go();
} }
});
}
Selectable<LastBottomNavStateData> getLastBottomNavState() { Selectable<LastBottomNavStateData> getLastBottomNavState() {
return select(lastBottomNavState)..where((tbl) => tbl.id.equals(1)); return select(lastBottomNavState)..where((tbl) => tbl.id.equals(1));

View File

@@ -4596,7 +4596,7 @@ abstract class _$SubtracksDatabase extends GeneratedDatabase {
)); ));
} }
Selectable<String> albumIdsWithDownloaded(int sourceId) { Selectable<String> albumIdsWithDownloadStatus(int sourceId) {
return customSelect( return customSelect(
'SELECT albums.id FROM albums JOIN songs ON songs.source_id = albums.source_id AND songs.album_id = albums.id WHERE albums.source_id = ?1 AND(songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)GROUP BY albums.id', 'SELECT albums.id FROM albums JOIN songs ON songs.source_id = albums.source_id AND songs.album_id = albums.id WHERE albums.source_id = ?1 AND(songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)GROUP BY albums.id',
variables: [ variables: [
@@ -4608,6 +4608,32 @@ abstract class _$SubtracksDatabase extends GeneratedDatabase {
}).map((QueryRow row) => row.read<String>('id')); }).map((QueryRow row) => row.read<String>('id'));
} }
Selectable<String> artistIdsWithDownloadStatus(int sourceId) {
return customSelect(
'SELECT artists.id FROM artists LEFT JOIN albums ON artists.source_id = albums.source_id AND artists.id = albums.artist_id LEFT JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id WHERE artists.source_id = ?1 AND(songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)GROUP BY artists.id',
variables: [
Variable<int>(sourceId)
],
readsFrom: {
artists,
albums,
songs,
}).map((QueryRow row) => row.read<String>('id'));
}
Selectable<String> playlistIdsWithDownloadStatus(int sourceId) {
return customSelect(
'SELECT playlists.id FROM playlists LEFT JOIN playlist_songs ON playlist_songs.source_id = playlists.source_id AND playlist_songs.playlist_id = playlists.id LEFT JOIN songs ON playlist_songs.source_id = songs.source_id AND playlist_songs.song_id = songs.id WHERE playlists.source_id = ?1 AND(songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)GROUP BY playlists.id',
variables: [
Variable<int>(sourceId)
],
readsFrom: {
playlists,
playlistSongs,
songs,
}).map((QueryRow row) => row.read<String>('id'));
}
Selectable<int> searchArtists(String query, int limit, int offset) { Selectable<int> searchArtists(String query, int limit, int offset) {
return customSelect( return customSelect(
'SELECT "rowid" FROM artists_fts WHERE artists_fts MATCH ?1 ORDER BY rank LIMIT ?2 OFFSET ?3', 'SELECT "rowid" FROM artists_fts WHERE artists_fts MATCH ?1 ORDER BY rank LIMIT ?2 OFFSET ?3',

View File

@@ -244,7 +244,7 @@ allSubsonicSources WITH SubsonicSettings:
FROM sources FROM sources
JOIN subsonic_sources ON subsonic_sources.source_id = sources.id; JOIN subsonic_sources ON subsonic_sources.source_id = sources.id;
albumIdsWithDownloaded: albumIdsWithDownloadStatus:
SELECT albums.id SELECT albums.id
FROM albums FROM albums
JOIN songs on songs.source_id = albums.source_id AND songs.album_id = albums.id JOIN songs on songs.source_id = albums.source_id AND songs.album_id = albums.id
@@ -253,6 +253,26 @@ albumIdsWithDownloaded:
AND (songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL) AND (songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)
GROUP BY albums.id; GROUP BY albums.id;
artistIdsWithDownloadStatus:
SELECT artists.id
FROM artists
LEFT JOIN albums ON artists.source_id = albums.source_id AND artists.id = albums.artist_id
LEFT JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
WHERE
artists.source_id = :source_id
AND (songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)
GROUP BY artists.id;
playlistIdsWithDownloadStatus:
SELECT playlists.id
FROM playlists
LEFT JOIN playlist_songs ON playlist_songs.source_id = playlists.source_id AND playlist_songs.playlist_id = playlists.id
LEFT JOIN songs ON playlist_songs.source_id = songs.source_id AND playlist_songs.song_id = songs.id
WHERE
playlists.source_id = :source_id
AND (songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)
GROUP BY playlists.id;
searchArtists: searchArtists:
SELECT rowid SELECT rowid
FROM artists_fts FROM artists_fts

View File

@@ -192,5 +192,15 @@
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Passwort als Klartext senden (Veraltet, stellen Sie sicher, dass Ihre Verbindung sicher ist!)", "settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Passwort als Klartext senden (Veraltet, stellen Sie sicher, dass Ihre Verbindung sicher ist!)",
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {}, "@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
"settingsServersOptionsForcePlaintextPasswordTitle": "Erzwinge Klartextpasswort", "settingsServersOptionsForcePlaintextPasswordTitle": "Erzwinge Klartextpasswort",
"@settingsServersOptionsForcePlaintextPasswordTitle": {} "@settingsServersOptionsForcePlaintextPasswordTitle": {},
"actionsDelete": "Löschen",
"@actionsDelete": {},
"actionsDownload": "Herunterladen",
"@actionsDownload": {},
"actionsDownloadCancel": "Download abbrechen",
"@actionsDownloadCancel": {},
"controlsShuffle": "Zufall",
"@controlsShuffle": {},
"actionsCancel": "Abbrechen",
"@actionsCancel": {}
} }

View File

@@ -1,7 +1,7 @@
{ {
"actionsStar": "Estrella", "actionsStar": "Favorito",
"@actionsStar": {}, "@actionsStar": {},
"actionsUnstar": "Retirar estrella", "actionsUnstar": "Retirar favorito",
"@actionsUnstar": {}, "@actionsUnstar": {},
"messagesNothingHere": "Nada aquí…", "messagesNothingHere": "Nada aquí…",
"@messagesNothingHere": {}, "@messagesNothingHere": {},
@@ -192,5 +192,19 @@
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Enviar contraseña en texto plano (¡legado, asegúrese de que su conexión sea segura!)", "settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Enviar contraseña en texto plano (¡legado, asegúrese de que su conexión sea segura!)",
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {}, "@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
"settingsServersOptionsForcePlaintextPasswordTitle": "Forzar contraseña de texto sin formato", "settingsServersOptionsForcePlaintextPasswordTitle": "Forzar contraseña de texto sin formato",
"@settingsServersOptionsForcePlaintextPasswordTitle": {} "@settingsServersOptionsForcePlaintextPasswordTitle": {},
"actionsDelete": "Borrar",
"@actionsDelete": {},
"actionsOk": "Ok",
"@actionsOk": {},
"actionsDownload": "Descargar",
"@actionsDownload": {},
"actionsDownloadCancel": "Anular descargar",
"@actionsDownloadCancel": {},
"controlsShuffle": "Reproducir aleatoriamente",
"@controlsShuffle": {},
"actionsCancel": "Cancelar",
"@actionsCancel": {},
"actionsDownloadDelete": "Eliminar descargado",
"@actionsDownloadDelete": {}
} }

View File

@@ -192,5 +192,85 @@
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Enviar contrasinal en texto plano (herdado, pon coidado en que a conexión sexa segura!)", "settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Enviar contrasinal en texto plano (herdado, pon coidado en que a conexión sexa segura!)",
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {}, "@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
"settingsServersOptionsForcePlaintextPasswordTitle": "Forzar contrasinal en texto plano", "settingsServersOptionsForcePlaintextPasswordTitle": "Forzar contrasinal en texto plano",
"@settingsServersOptionsForcePlaintextPasswordTitle": {} "@settingsServersOptionsForcePlaintextPasswordTitle": {},
"actionsCancel": "Cancelar",
"@actionsCancel": {},
"actionsDelete": "Eliminar",
"@actionsDelete": {},
"actionsDownload": "Descargar",
"@actionsDownload": {},
"actionsDownloadCancel": "Cancelar a descarga",
"@actionsDownloadCancel": {},
"actionsDownloadDelete": "Eliminar o descargado",
"@actionsDownloadDelete": {},
"actionsOk": "OK",
"@actionsOk": {},
"controlsShuffle": "Barallar",
"@controlsShuffle": {},
"resourcesAlbumCount": "{count,plural, =1{{count} álbum} other{{count} álbums}}",
"@resourcesAlbumCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"resourcesArtistCount": "{count,plural, =1{{count} artista} other{{count} artistas}}",
"@resourcesArtistCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"resourcesFilterAlbum": "Álbum",
"@resourcesFilterAlbum": {},
"resourcesFilterArtist": "Artista",
"@resourcesFilterArtist": {},
"resourcesFilterOwner": "Dono",
"@resourcesFilterOwner": {},
"resourcesFilterYear": "Ano",
"@resourcesFilterYear": {},
"resourcesPlaylistCount": "{count,plural, =1{{count} lista} other{{count} listas}}",
"@resourcesPlaylistCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"resourcesSongCount": "{count,plural, =1{{count} canción} other{{count} cancións}}",
"@resourcesSongCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"resourcesSongListDeleteAllContent": "Vas eliminar todas as cancións descargadas.",
"@resourcesSongListDeleteAllContent": {},
"resourcesSongListDeleteAllTitle": "Eliminar descargas?",
"@resourcesSongListDeleteAllTitle": {},
"resourcesSortByAlbum": "Álbum",
"@resourcesSortByAlbum": {},
"resourcesSortByAlbumCount": "Número de álbums",
"@resourcesSortByAlbumCount": {},
"settingsAboutActionsSupport": "Axuda ao desenvolvemento 💜",
"@settingsAboutActionsSupport": {},
"settingsNetworkOptionsOfflineMode": "Modo sen conexión",
"@settingsNetworkOptionsOfflineMode": {},
"settingsNetworkOptionsOfflineModeOff": "Usa internet para sincr. música.",
"@settingsNetworkOptionsOfflineModeOff": {},
"settingsNetworkOptionsOfflineModeOn": "Non usar internet para sincr. ou reproducir música.",
"@settingsNetworkOptionsOfflineModeOn": {},
"settingsNetworkOptionsStreamFormat": "Modo de reprodución preferido",
"@settingsNetworkOptionsStreamFormat": {},
"settingsNetworkOptionsStreamFormatServerDefault": "Usar por defecto do servidor",
"@settingsNetworkOptionsStreamFormatServerDefault": {},
"settingsServersFieldsName": "Nome",
"@settingsServersFieldsName": {},
"resourcesSortByTitle": "Título",
"@resourcesSortByTitle": {},
"resourcesSortByUpdated": "Actualizado recentemente",
"@resourcesSortByUpdated": {}
} }

View File

@@ -192,5 +192,39 @@
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "密码以明文发送(不推荐,注意链接安全!)", "settingsServersOptionsForcePlaintextPasswordDescriptionOn": "密码以明文发送(不推荐,注意链接安全!)",
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {}, "@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
"settingsServersOptionsForcePlaintextPasswordTitle": "强制使用明文密码", "settingsServersOptionsForcePlaintextPasswordTitle": "强制使用明文密码",
"@settingsServersOptionsForcePlaintextPasswordTitle": {} "@settingsServersOptionsForcePlaintextPasswordTitle": {},
"actionsDownload": "下载",
"@actionsDownload": {},
"actionsDownloadCancel": "取消下载",
"@actionsDownloadCancel": {},
"actionsDownloadDelete": "删除已下载",
"@actionsDownloadDelete": {},
"actionsOk": "确定",
"@actionsOk": {},
"resourcesFilterArtist": "歌手",
"@resourcesFilterArtist": {},
"resourcesFilterOwner": "所有者",
"@resourcesFilterOwner": {},
"resourcesSongListDeleteAllTitle": "删除下载?",
"@resourcesSongListDeleteAllTitle": {},
"resourcesSortByAlbum": "专辑",
"@resourcesSortByAlbum": {},
"resourcesSortByAlbumCount": "专辑数量",
"@resourcesSortByAlbumCount": {},
"resourcesSortByUpdated": "最近添加",
"@resourcesSortByUpdated": {},
"settingsAboutActionsSupport": "支持开发者",
"@settingsAboutActionsSupport": {},
"resourcesFilterAlbum": "专辑",
"@resourcesFilterAlbum": {},
"resourcesSortByTitle": "标题",
"@resourcesSortByTitle": {},
"actionsCancel": "取消",
"@actionsCancel": {},
"actionsDelete": "删除",
"@actionsDelete": {},
"resourcesFilterYear": "年份",
"@resourcesFilterYear": {},
"resourcesSongListDeleteAllContent": "该操作会删除所有已下载的歌曲文件。",
"@resourcesSongListDeleteAllContent": {}
} }

View File

@@ -68,7 +68,8 @@ enum QueueContextType {
album('album'), album('album'),
playlist('playlist'), playlist('playlist'),
library('library'), library('library'),
genre('genre'); genre('genre'),
artist('artist');
const QueueContextType(this.value); const QueueContextType(this.value);
final String value; final String value;

View File

@@ -28,6 +28,7 @@ const _$QueueContextTypeEnumMap = {
QueueContextType.playlist: 'playlist', QueueContextType.playlist: 'playlist',
QueueContextType.library: 'library', QueueContextType.library: 'library',
QueueContextType.genre: 'genre', QueueContextType.genre: 'genre',
QueueContextType.artist: 'artist',
}; };
_$_MediaItemData _$$_MediaItemDataFromJson(Map<String, dynamic> json) => _$_MediaItemData _$$_MediaItemDataFromJson(Map<String, dynamic> json) =>

View File

@@ -214,7 +214,7 @@ class DownloadService extends _$DownloadService {
Future<void> deleteAll(int sourceId) async { Future<void> deleteAll(int sourceId) async {
final db = ref.read(databaseProvider); final db = ref.read(databaseProvider);
final albumIds = await db.albumIdsWithDownloaded(sourceId).get(); final albumIds = await db.albumIdsWithDownloadStatus(sourceId).get();
for (var id in albumIds) { for (var id in albumIds) {
await deleteAlbum(await (db.albumById(sourceId, id)).getSingle()); await deleteAlbum(await (db.albumById(sourceId, id)).getSingle());
} }

View File

@@ -6,7 +6,7 @@ part of 'download_service.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$downloadServiceHash() => r'92e963b5c070f4d1edb0cd81899b16393c2b9a70'; String _$downloadServiceHash() => r'c72c49f980e307f3013467e76b6564d14a34a736';
/// See also [DownloadService]. /// See also [DownloadService].
@ProviderFor(DownloadService) @ProviderFor(DownloadService)

View File

@@ -31,7 +31,7 @@ class SyncService extends _$SyncService {
final source = ref.read(musicSourceProvider); final source = ref.read(musicSourceProvider);
final db = ref.read(databaseProvider); final db = ref.read(databaseProvider);
final ids = <String>[]; final ids = <String>{};
await for (var artists in source.allArtists()) { await for (var artists in source.allArtists()) {
ids.addAll(artists.map((e) => e.id.value)); ids.addAll(artists.map((e) => e.id.value));
await db.saveArtists(artists); await db.saveArtists(artists);
@@ -44,7 +44,7 @@ class SyncService extends _$SyncService {
final source = ref.read(musicSourceProvider); final source = ref.read(musicSourceProvider);
final db = ref.read(databaseProvider); final db = ref.read(databaseProvider);
final ids = <String>[]; final ids = <String>{};
await for (var albums in source.allAlbums()) { await for (var albums in source.allAlbums()) {
ids.addAll(albums.map((e) => e.id.value)); ids.addAll(albums.map((e) => e.id.value));
await db.saveAlbums(albums); await db.saveAlbums(albums);
@@ -57,7 +57,7 @@ class SyncService extends _$SyncService {
final source = ref.read(musicSourceProvider); final source = ref.read(musicSourceProvider);
final db = ref.read(databaseProvider); final db = ref.read(databaseProvider);
final ids = <String>[]; final ids = <String>{};
await for (var playlists in source.allPlaylists()) { await for (var playlists in source.allPlaylists()) {
ids.addAll(playlists.map((e) => e.playist.id.value)); ids.addAll(playlists.map((e) => e.playist.id.value));
await db.savePlaylists(playlists); await db.savePlaylists(playlists);
@@ -70,7 +70,7 @@ class SyncService extends _$SyncService {
final source = ref.read(musicSourceProvider); final source = ref.read(musicSourceProvider);
final db = ref.read(databaseProvider); final db = ref.read(databaseProvider);
final ids = <String>[]; final ids = <String>{};
await for (var songs in source.allSongs()) { await for (var songs in source.allSongs()) {
ids.addAll(songs.map((e) => e.id.value)); ids.addAll(songs.map((e) => e.id.value));
await db.saveSongs(songs); await db.saveSongs(songs);

View File

@@ -6,7 +6,7 @@ part of 'sync_service.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$syncServiceHash() => r'2b8da374c3143bc56f17115440d57bc70468a17e'; String _$syncServiceHash() => r'58ebee4e6f055b64ee6789ae43d63c0e15c679e0';
/// See also [SyncService]. /// See also [SyncService].
@ProviderFor(SyncService) @ProviderFor(SyncService)

View File

@@ -82,10 +82,4 @@ class MusicSource implements BaseMusicSource {
@override @override
Uri streamUri(String songId) => _source.streamUri(songId); Uri streamUri(String songId) => _source.streamUri(songId);
@override
bool operator ==(other) => other is BaseMusicSource && (other.id == id);
@override
int get hashCode => id;
} }