feature(web): Added option to download file and copy stream url (#209)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-02-01 15:22:22 +01:00 committed by GitHub
parent 54babaec89
commit bd8faf2f6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 61 additions and 18 deletions

View file

@ -1149,5 +1149,14 @@
"mediaSegmentOutro": "Outro", "mediaSegmentOutro": "Outro",
"mediaSegmentIntro": "Intro", "mediaSegmentIntro": "Intro",
"errorLogs": "Error logs", "errorLogs": "Error logs",
"external": "External" "external": "External",
"downloadFile": "Download {type}",
"@downloadFile": {
"placeholders": {
"type": {
"type": "String"
}
}
},
"copyStreamUrl": "Copy stream url"
} }

View file

@ -38,13 +38,9 @@ class AccountModel with _$AccountModel {
factory AccountModel.fromJson(Map<String, dynamic> json) => _$AccountModelFromJson(json); factory AccountModel.fromJson(Map<String, dynamic> json) => _$AccountModelFromJson(json);
String get server { String get server => credentials.server;
return credentials.server;
}
bool get canDownload { bool get canDownload => (policy?.enableContentDownloading ?? false);
return (policy?.enableContentDownloading ?? false) && !kIsWeb;
}
//Check if it's the same account on the same server //Check if it's the same account on the same server
bool sameIdentity(AccountModel other) { bool sameIdentity(AccountModel other) {

View file

@ -5,6 +5,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:fladder/jellyfin/enum_models.dart'; import 'package:fladder/jellyfin/enum_models.dart';
import 'package:fladder/models/account_model.dart'; import 'package:fladder/models/account_model.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/library_filters_model.dart'; import 'package:fladder/models/library_filters_model.dart';
import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/api_provider.dart';
@ -172,4 +173,7 @@ class User extends _$User {
} }
void deleteAllFilters() => state = state?.copyWith(savedFilters: []); void deleteAllFilters() => state = state?.copyWith(savedFilters: []);
String? createDownloadUrl(ItemBaseModel item) =>
Uri.encodeFull("${state?.server}/Items/${item.id}/Download?api_key=${state?.credentials.token}");
} }

View file

@ -0,0 +1,11 @@
import 'package:universal_html/html.dart' as html;
Future<void> downloadFile(String url) async {
try {
html.AnchorElement anchorElement = html.AnchorElement(href: url);
anchorElement.download = url;
anchorElement.click();
} catch (e) {
print('Download error: $e');
}
}

View file

@ -1,4 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:ficonsax/ficonsax.dart'; import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -19,6 +21,7 @@ import 'package:fladder/screens/playlists/add_to_playlists.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/screens/syncing/sync_button.dart'; import 'package:fladder/screens/syncing/sync_button.dart';
import 'package:fladder/screens/syncing/sync_item_details.dart'; import 'package:fladder/screens/syncing/sync_item_details.dart';
import 'package:fladder/util/file_downloader.dart';
import 'package:fladder/util/item_base_model/play_item_helpers.dart'; import 'package:fladder/util/item_base_model/play_item_helpers.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/refresh_state.dart';
@ -76,6 +79,7 @@ extension ItemBaseModelExtensions on ItemBaseModel {
syncAble && syncAble &&
(canDownload ?? false); (canDownload ?? false);
final syncedItem = ref.read(syncProvider.notifier).getSyncedItem(this); final syncedItem = ref.read(syncProvider.notifier).getSyncedItem(this);
final downloadUrl = ref.read(userProvider.notifier).createDownloadUrl(this);
return [ return [
if (!exclude.contains(ItemActions.play)) if (!exclude.contains(ItemActions.play))
if (playAble) if (playAble)
@ -197,19 +201,38 @@ extension ItemBaseModelExtensions on ItemBaseModel {
}, },
label: Text(context.localized.refreshMetadata), label: Text(context.localized.refreshMetadata),
), ),
if (!exclude.contains(ItemActions.download) && downloadEnabled) if (!exclude.contains(ItemActions.download) && downloadEnabled) ...[
if (syncedItem == null) if (!kIsWeb)
if (syncedItem == null)
ItemActionButton(
icon: const Icon(IconsaxOutline.arrow_down_2),
label: Text(context.localized.sync),
action: () => ref.read(syncProvider.notifier).addSyncItem(context, this),
)
else
ItemActionButton(
icon: IgnorePointer(child: SyncButton(item: this, syncedItem: syncedItem)),
action: () => showSyncItemDetails(context, syncedItem, ref),
label: Text(context.localized.syncDetails),
)
else if (downloadUrl != null) ...[
ItemActionButton( ItemActionButton(
icon: const Icon(IconsaxOutline.arrow_down_2), icon: const Icon(IconsaxOutline.document_download),
label: Text(context.localized.sync), action: () => downloadFile(downloadUrl),
action: () => ref.read(syncProvider.notifier).addSyncItem(context, this), label: Text(context.localized.downloadFile(type.label(context).toLowerCase())),
)
else
ItemActionButton(
icon: IgnorePointer(child: SyncButton(item: this, syncedItem: syncedItem)),
action: () => showSyncItemDetails(context, syncedItem, ref),
label: Text(context.localized.syncDetails),
), ),
ItemActionButton(
icon: const Icon(IconsaxOutline.link_21),
action: () async {
await Clipboard.setData(ClipboardData(text: downloadUrl));
if (context.mounted) {
fladderSnackbar(context, title: "Copied URL to clipboard");
}
},
label: Text(context.localized.copyStreamUrl),
)
],
],
if (canDelete == true) if (canDelete == true)
ItemActionButton( ItemActionButton(
icon: Container( icon: Container(