946 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			946 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'dart:io';
 | 
						|
import 'dart:math' as math;
 | 
						|
 | 
						|
import 'package:cached_network_image/cached_network_image.dart';
 | 
						|
import 'package:easy_localization/easy_localization.dart';
 | 
						|
import 'package:file_saver/file_saver.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
						|
import 'package:gap/gap.dart';
 | 
						|
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
						|
import 'package:island/models/file.dart';
 | 
						|
import 'package:island/pods/config.dart';
 | 
						|
import 'package:island/pods/network.dart';
 | 
						|
import 'package:island/services/time.dart';
 | 
						|
import 'package:island/utils/format.dart';
 | 
						|
import 'package:island/widgets/alert.dart';
 | 
						|
import 'package:island/widgets/content/audio.dart';
 | 
						|
import 'package:material_symbols_icons/symbols.dart';
 | 
						|
import 'package:path/path.dart' show extension;
 | 
						|
import 'package:path_provider/path_provider.dart';
 | 
						|
import 'package:styled_widget/styled_widget.dart';
 | 
						|
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
 | 
						|
import 'package:island/widgets/data_saving_gate.dart';
 | 
						|
import 'package:island/widgets/content/file_info_sheet.dart';
 | 
						|
 | 
						|
import 'image.dart';
 | 
						|
import 'video.dart';
 | 
						|
 | 
						|
class CloudFileWidget extends HookConsumerWidget {
 | 
						|
  final SnCloudFile item;
 | 
						|
  final BoxFit fit;
 | 
						|
  final String? heroTag;
 | 
						|
  final bool noBlurhash;
 | 
						|
  final bool useInternalGate;
 | 
						|
  const CloudFileWidget({
 | 
						|
    super.key,
 | 
						|
    required this.item,
 | 
						|
    this.fit = BoxFit.cover,
 | 
						|
    this.heroTag,
 | 
						|
    this.noBlurhash = false,
 | 
						|
    this.useInternalGate = true,
 | 
						|
  });
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final dataSaving = ref.watch(
 | 
						|
      appSettingsNotifierProvider.select((s) => s.dataSavingMode),
 | 
						|
    );
 | 
						|
    final serverUrl = ref.watch(serverUrlProvider);
 | 
						|
    final uri = '$serverUrl/drive/files/${item.id}';
 | 
						|
 | 
						|
    final unlocked = useState(false);
 | 
						|
 | 
						|
    final meta = item.fileMeta is Map ? (item.fileMeta as Map) : const {};
 | 
						|
    final blurHash = noBlurhash ? null : (meta['blur'] as String?);
 | 
						|
    var ratio = meta['ratio'] is num ? (meta['ratio'] as num).toDouble() : 1.0;
 | 
						|
    if (ratio == 0) ratio = 1.0;
 | 
						|
 | 
						|
    Widget cloudImage() =>
 | 
						|
        UniversalImage(uri: uri, blurHash: blurHash, fit: fit);
 | 
						|
    Widget cloudVideo() => CloudVideoWidget(item: item);
 | 
						|
 | 
						|
    Widget dataPlaceHolder(IconData icon) => _DataSavingPlaceholder(
 | 
						|
      icon: icon,
 | 
						|
      onTap: () {
 | 
						|
        unlocked.value = true;
 | 
						|
      },
 | 
						|
    );
 | 
						|
 | 
						|
    if (item.mimeType == 'application/pdf') {
 | 
						|
      final pdfViewer = useMemoized(() => SfPdfViewer.network(uri), [uri]);
 | 
						|
 | 
						|
      Future<void> downloadFile() async {
 | 
						|
        try {
 | 
						|
          showSnackBar('Downloading file...');
 | 
						|
 | 
						|
          final client = ref.read(apiClientProvider);
 | 
						|
          final tempDir = await getTemporaryDirectory();
 | 
						|
          var extName = extension(item.name).trim();
 | 
						|
          if (extName.isEmpty) {
 | 
						|
            extName = item.mimeType?.split('/').lastOrNull ?? 'pdf';
 | 
						|
          }
 | 
						|
          final filePath = '${tempDir.path}/${item.id}.$extName';
 | 
						|
 | 
						|
          await client.download(
 | 
						|
            '/drive/files/${item.id}',
 | 
						|
            filePath,
 | 
						|
            queryParameters: {'original': true},
 | 
						|
          );
 | 
						|
 | 
						|
          await FileSaver.instance.saveFile(
 | 
						|
            name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
 | 
						|
            file: File(filePath),
 | 
						|
          );
 | 
						|
          showSnackBar('File saved to downloads');
 | 
						|
        } catch (e) {
 | 
						|
          showErrorAlert(e);
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      return Container(
 | 
						|
        height: 400,
 | 
						|
        decoration: BoxDecoration(
 | 
						|
          border: Border.all(
 | 
						|
            color: Theme.of(context).colorScheme.outline,
 | 
						|
            width: 1,
 | 
						|
          ),
 | 
						|
          borderRadius: BorderRadius.circular(8),
 | 
						|
        ),
 | 
						|
        child: Stack(
 | 
						|
          children: [
 | 
						|
            pdfViewer,
 | 
						|
            Positioned(
 | 
						|
              top: 8,
 | 
						|
              left: 8,
 | 
						|
              child: Container(
 | 
						|
                padding: const EdgeInsets.all(4),
 | 
						|
                decoration: BoxDecoration(
 | 
						|
                  color: Colors.black54,
 | 
						|
                  borderRadius: BorderRadius.circular(8),
 | 
						|
                ),
 | 
						|
                child: Row(
 | 
						|
                  mainAxisSize: MainAxisSize.min,
 | 
						|
                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                  spacing: 7,
 | 
						|
                  children: [
 | 
						|
                    Icon(
 | 
						|
                      Symbols.picture_as_pdf,
 | 
						|
                      size: 16,
 | 
						|
                      color: Colors.white,
 | 
						|
                    ).padding(top: 2),
 | 
						|
                    Column(
 | 
						|
                      mainAxisSize: MainAxisSize.min,
 | 
						|
                      crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                      children: [
 | 
						|
                        Text(
 | 
						|
                          item.name,
 | 
						|
                          style: const TextStyle(
 | 
						|
                            color: Colors.white,
 | 
						|
                            fontSize: 12,
 | 
						|
                          ),
 | 
						|
                          maxLines: 1,
 | 
						|
                          overflow: TextOverflow.ellipsis,
 | 
						|
                        ),
 | 
						|
                        Text(
 | 
						|
                          formatFileSize(item.size),
 | 
						|
                          style: const TextStyle(
 | 
						|
                            color: Colors.white,
 | 
						|
                            fontSize: 9,
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                  ],
 | 
						|
                ).padding(vertical: 4, horizontal: 8),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
            Positioned(
 | 
						|
              top: 8,
 | 
						|
              right: 8,
 | 
						|
              child: Container(
 | 
						|
                padding: const EdgeInsets.all(8),
 | 
						|
                decoration: BoxDecoration(
 | 
						|
                  color: Colors.black54,
 | 
						|
                  borderRadius: BorderRadius.circular(8),
 | 
						|
                ),
 | 
						|
                child: Row(
 | 
						|
                  mainAxisSize: MainAxisSize.min,
 | 
						|
                  spacing: 4,
 | 
						|
                  children: [
 | 
						|
                    IconButton(
 | 
						|
                      icon: const Icon(
 | 
						|
                        Symbols.download,
 | 
						|
                        color: Colors.white,
 | 
						|
                        size: 16,
 | 
						|
                      ),
 | 
						|
                      onPressed: downloadFile,
 | 
						|
                      padding: EdgeInsets.all(4),
 | 
						|
                      constraints: const BoxConstraints(),
 | 
						|
                    ),
 | 
						|
                    IconButton(
 | 
						|
                      icon: const Icon(
 | 
						|
                        Symbols.info,
 | 
						|
                        color: Colors.white,
 | 
						|
                        size: 16,
 | 
						|
                      ),
 | 
						|
                      onPressed: () {
 | 
						|
                        showModalBottomSheet(
 | 
						|
                          useRootNavigator: true,
 | 
						|
                          context: context,
 | 
						|
                          isScrollControlled: true,
 | 
						|
                          builder: (context) => FileInfoSheet(item: item),
 | 
						|
                        );
 | 
						|
                      },
 | 
						|
                      padding: EdgeInsets.all(4),
 | 
						|
                      constraints: const BoxConstraints(),
 | 
						|
                    ),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (item.mimeType?.startsWith('text/') == true) {
 | 
						|
      final textFuture = useMemoized(
 | 
						|
        () => ref
 | 
						|
            .read(apiClientProvider)
 | 
						|
            .get(uri)
 | 
						|
            .then((response) => response.data as String),
 | 
						|
        [uri],
 | 
						|
      );
 | 
						|
 | 
						|
      Future<void> downloadFile() async {
 | 
						|
        try {
 | 
						|
          showSnackBar('Downloading file...');
 | 
						|
 | 
						|
          final client = ref.read(apiClientProvider);
 | 
						|
          final tempDir = await getTemporaryDirectory();
 | 
						|
          var extName = extension(item.name).trim();
 | 
						|
          if (extName.isEmpty) {
 | 
						|
            extName = item.mimeType?.split('/').lastOrNull ?? 'txt';
 | 
						|
          }
 | 
						|
          final filePath = '${tempDir.path}/${item.id}.$extName';
 | 
						|
 | 
						|
          await client.download(
 | 
						|
            '/drive/files/${item.id}',
 | 
						|
            filePath,
 | 
						|
            queryParameters: {'original': true},
 | 
						|
          );
 | 
						|
 | 
						|
          await FileSaver.instance.saveFile(
 | 
						|
            name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
 | 
						|
            file: File(filePath),
 | 
						|
          );
 | 
						|
          showSnackBar('File saved to downloads');
 | 
						|
        } catch (e) {
 | 
						|
          showErrorAlert(e);
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      return Container(
 | 
						|
        height: 400,
 | 
						|
        decoration: BoxDecoration(
 | 
						|
          border: Border.all(
 | 
						|
            color: Theme.of(context).colorScheme.outline,
 | 
						|
            width: 1,
 | 
						|
          ),
 | 
						|
          borderRadius: BorderRadius.circular(8),
 | 
						|
        ),
 | 
						|
        child: Stack(
 | 
						|
          children: [
 | 
						|
            FutureBuilder<String>(
 | 
						|
              future: textFuture,
 | 
						|
              builder: (context, snapshot) {
 | 
						|
                if (snapshot.connectionState == ConnectionState.waiting) {
 | 
						|
                  return const Center(child: CircularProgressIndicator());
 | 
						|
                } else if (snapshot.hasError) {
 | 
						|
                  return Center(
 | 
						|
                    child: Text('Error loading text: ${snapshot.error}'),
 | 
						|
                  );
 | 
						|
                } else if (snapshot.hasData) {
 | 
						|
                  return SingleChildScrollView(
 | 
						|
                    padding: const EdgeInsets.fromLTRB(20, 20 + 48, 20, 20),
 | 
						|
                    child: SelectableText(
 | 
						|
                      snapshot.data!,
 | 
						|
                      style: const TextStyle(
 | 
						|
                        fontFamily: 'monospace',
 | 
						|
                        fontSize: 14,
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  );
 | 
						|
                }
 | 
						|
                return const Center(child: Text('No content'));
 | 
						|
              },
 | 
						|
            ),
 | 
						|
            Positioned(
 | 
						|
              top: 8,
 | 
						|
              left: 8,
 | 
						|
              child: Container(
 | 
						|
                padding: const EdgeInsets.all(4),
 | 
						|
                decoration: BoxDecoration(
 | 
						|
                  color: Colors.black54,
 | 
						|
                  borderRadius: BorderRadius.circular(8),
 | 
						|
                ),
 | 
						|
                child: Row(
 | 
						|
                  mainAxisSize: MainAxisSize.min,
 | 
						|
                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                  spacing: 7,
 | 
						|
                  children: [
 | 
						|
                    Icon(
 | 
						|
                      Symbols.file_present,
 | 
						|
                      size: 16,
 | 
						|
                      color: Colors.white,
 | 
						|
                    ).padding(top: 2),
 | 
						|
                    Column(
 | 
						|
                      mainAxisSize: MainAxisSize.min,
 | 
						|
                      crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                      children: [
 | 
						|
                        Text(
 | 
						|
                          item.name,
 | 
						|
                          style: const TextStyle(
 | 
						|
                            color: Colors.white,
 | 
						|
                            fontSize: 12,
 | 
						|
                          ),
 | 
						|
                          maxLines: 1,
 | 
						|
                          overflow: TextOverflow.ellipsis,
 | 
						|
                        ),
 | 
						|
                        Text(
 | 
						|
                          formatFileSize(item.size),
 | 
						|
                          style: const TextStyle(
 | 
						|
                            color: Colors.white,
 | 
						|
                            fontSize: 9,
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                  ],
 | 
						|
                ).padding(vertical: 4, horizontal: 8),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
            Positioned(
 | 
						|
              top: 8,
 | 
						|
              right: 8,
 | 
						|
              child: Container(
 | 
						|
                padding: const EdgeInsets.all(8),
 | 
						|
                decoration: BoxDecoration(
 | 
						|
                  color: Colors.black54,
 | 
						|
                  borderRadius: BorderRadius.circular(8),
 | 
						|
                ),
 | 
						|
                child: Row(
 | 
						|
                  mainAxisSize: MainAxisSize.min,
 | 
						|
                  spacing: 4,
 | 
						|
                  children: [
 | 
						|
                    IconButton(
 | 
						|
                      icon: const Icon(
 | 
						|
                        Symbols.download,
 | 
						|
                        color: Colors.white,
 | 
						|
                        size: 16,
 | 
						|
                      ),
 | 
						|
                      onPressed: downloadFile,
 | 
						|
                      padding: EdgeInsets.all(4),
 | 
						|
                      constraints: const BoxConstraints(),
 | 
						|
                    ),
 | 
						|
                    IconButton(
 | 
						|
                      icon: const Icon(
 | 
						|
                        Symbols.info,
 | 
						|
                        color: Colors.white,
 | 
						|
                        size: 16,
 | 
						|
                      ),
 | 
						|
                      onPressed: () {
 | 
						|
                        showModalBottomSheet(
 | 
						|
                          useRootNavigator: true,
 | 
						|
                          context: context,
 | 
						|
                          isScrollControlled: true,
 | 
						|
                          builder: (context) => FileInfoSheet(item: item),
 | 
						|
                        );
 | 
						|
                      },
 | 
						|
                      padding: EdgeInsets.all(4),
 | 
						|
                      constraints: const BoxConstraints(),
 | 
						|
                    ),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    var content = switch (item.mimeType?.split('/').firstOrNull) {
 | 
						|
      'image' => AspectRatio(
 | 
						|
        aspectRatio: ratio,
 | 
						|
        child:
 | 
						|
            (useInternalGate && dataSaving && !unlocked.value)
 | 
						|
                ? dataPlaceHolder(Symbols.image)
 | 
						|
                : cloudImage(),
 | 
						|
      ),
 | 
						|
      'video' => AspectRatio(
 | 
						|
        aspectRatio: ratio,
 | 
						|
        child:
 | 
						|
            (useInternalGate && dataSaving && !unlocked.value)
 | 
						|
                ? dataPlaceHolder(Symbols.play_arrow)
 | 
						|
                : cloudVideo(),
 | 
						|
      ),
 | 
						|
      'audio' => Center(
 | 
						|
        child: ConstrainedBox(
 | 
						|
          constraints: BoxConstraints(
 | 
						|
            maxWidth: math.min(360, MediaQuery.of(context).size.width * 0.8),
 | 
						|
          ),
 | 
						|
          child: UniversalAudio(uri: uri, filename: item.name),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
      _ => Builder(
 | 
						|
        builder: (context) {
 | 
						|
          Future<void> downloadFile() async {
 | 
						|
            try {
 | 
						|
              showSnackBar('Downloading file...');
 | 
						|
 | 
						|
              final client = ref.read(apiClientProvider);
 | 
						|
              final tempDir = await getTemporaryDirectory();
 | 
						|
              var extName = extension(item.name).trim();
 | 
						|
              if (extName.isEmpty) {
 | 
						|
                extName = item.mimeType?.split('/').lastOrNull ?? 'bin';
 | 
						|
              }
 | 
						|
              final filePath = '${tempDir.path}/${item.id}.$extName';
 | 
						|
 | 
						|
              await client.download(
 | 
						|
                '/drive/files/${item.id}',
 | 
						|
                filePath,
 | 
						|
                queryParameters: {'original': true},
 | 
						|
              );
 | 
						|
 | 
						|
              await FileSaver.instance.saveFile(
 | 
						|
                name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
 | 
						|
                file: File(filePath),
 | 
						|
              );
 | 
						|
              showSnackBar('File saved to downloads');
 | 
						|
            } catch (e) {
 | 
						|
              showErrorAlert(e);
 | 
						|
            }
 | 
						|
          }
 | 
						|
 | 
						|
          return Container(
 | 
						|
            decoration: BoxDecoration(
 | 
						|
              border: Border.all(
 | 
						|
                color: Theme.of(context).colorScheme.outline,
 | 
						|
                width: 1,
 | 
						|
              ),
 | 
						|
              borderRadius: BorderRadius.circular(8),
 | 
						|
            ),
 | 
						|
            child: Column(
 | 
						|
              mainAxisSize: MainAxisSize.min,
 | 
						|
              mainAxisAlignment: MainAxisAlignment.center,
 | 
						|
              children: [
 | 
						|
                Icon(
 | 
						|
                  Symbols.insert_drive_file,
 | 
						|
                  size: 48,
 | 
						|
                  color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
						|
                ),
 | 
						|
                const Gap(8),
 | 
						|
                Text(
 | 
						|
                  item.name,
 | 
						|
                  maxLines: 1,
 | 
						|
                  overflow: TextOverflow.ellipsis,
 | 
						|
                  style: TextStyle(
 | 
						|
                    fontSize: 14,
 | 
						|
                    color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                Text(
 | 
						|
                  formatFileSize(item.size),
 | 
						|
                  style: TextStyle(
 | 
						|
                    fontSize: 12,
 | 
						|
                    color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                const Gap(8),
 | 
						|
                Row(
 | 
						|
                  mainAxisSize: MainAxisSize.min,
 | 
						|
                  children: [
 | 
						|
                    TextButton.icon(
 | 
						|
                      onPressed: downloadFile,
 | 
						|
                      icon: const Icon(Symbols.download),
 | 
						|
                      label: Text('download').tr(),
 | 
						|
                    ),
 | 
						|
                    const Gap(8),
 | 
						|
                    TextButton.icon(
 | 
						|
                      onPressed: () {
 | 
						|
                        showModalBottomSheet(
 | 
						|
                          useRootNavigator: true,
 | 
						|
                          context: context,
 | 
						|
                          isScrollControlled: true,
 | 
						|
                          builder: (context) => FileInfoSheet(item: item),
 | 
						|
                        );
 | 
						|
                      },
 | 
						|
                      icon: const Icon(Symbols.info),
 | 
						|
                      label: Text('info').tr(),
 | 
						|
                    ),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ).padding(all: 8),
 | 
						|
          );
 | 
						|
        },
 | 
						|
      ),
 | 
						|
    };
 | 
						|
 | 
						|
    if (heroTag != null) {
 | 
						|
      content = Hero(tag: heroTag!, child: content);
 | 
						|
    }
 | 
						|
 | 
						|
    return content;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _DataSavingPlaceholder extends StatelessWidget {
 | 
						|
  final IconData icon;
 | 
						|
  final VoidCallback onTap;
 | 
						|
  const _DataSavingPlaceholder({required this.icon, required this.onTap});
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return GestureDetector(
 | 
						|
      onTap: onTap,
 | 
						|
      child: Container(
 | 
						|
        color: Colors.black26,
 | 
						|
        alignment: Alignment.center,
 | 
						|
        child: Column(
 | 
						|
          mainAxisSize: MainAxisSize.min,
 | 
						|
          children: [
 | 
						|
            Icon(
 | 
						|
              icon,
 | 
						|
              size: 36,
 | 
						|
              color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
						|
            ),
 | 
						|
            const Gap(8),
 | 
						|
            Text(
 | 
						|
              'dataSavingHint'.tr(),
 | 
						|
              style: Theme.of(context).textTheme.bodySmall,
 | 
						|
              textAlign: TextAlign.center,
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class CloudVideoWidget extends HookConsumerWidget {
 | 
						|
  final SnCloudFile item;
 | 
						|
  const CloudVideoWidget({super.key, required this.item});
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final open = useState(false);
 | 
						|
 | 
						|
    final serverUrl = ref.watch(serverUrlProvider);
 | 
						|
    final uri = '$serverUrl/drive/files/${item.id}';
 | 
						|
 | 
						|
    var ratio =
 | 
						|
        item.fileMeta?['ratio'] is num
 | 
						|
            ? item.fileMeta!['ratio'].toDouble()
 | 
						|
            : 1.0;
 | 
						|
    if (ratio == 0) ratio = 1.0;
 | 
						|
 | 
						|
    if (open.value) {
 | 
						|
      return UniversalVideo(uri: uri, aspectRatio: ratio, autoplay: true);
 | 
						|
    }
 | 
						|
 | 
						|
    return GestureDetector(
 | 
						|
      child: Stack(
 | 
						|
        children: [
 | 
						|
          UniversalImage(uri: '$uri?thumbnail=true'),
 | 
						|
          Positioned.fill(
 | 
						|
            child: Center(
 | 
						|
              child: const Icon(
 | 
						|
                Symbols.play_arrow,
 | 
						|
                fill: 1,
 | 
						|
                size: 32,
 | 
						|
                color: Colors.white,
 | 
						|
                shadows: [
 | 
						|
                  BoxShadow(
 | 
						|
                    color: Colors.black54,
 | 
						|
                    offset: Offset(1, 1),
 | 
						|
                    spreadRadius: 8,
 | 
						|
                    blurRadius: 8,
 | 
						|
                  ),
 | 
						|
                ],
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
          Positioned(
 | 
						|
            bottom: 0,
 | 
						|
            left: 0,
 | 
						|
            right: 0,
 | 
						|
            child: IgnorePointer(
 | 
						|
              child: Container(
 | 
						|
                height: 100,
 | 
						|
                decoration: BoxDecoration(
 | 
						|
                  gradient: LinearGradient(
 | 
						|
                    begin: Alignment.bottomCenter,
 | 
						|
                    end: Alignment.topCenter,
 | 
						|
                    colors: [
 | 
						|
                      Theme.of(context).colorScheme.surface.withOpacity(0.85),
 | 
						|
                      Colors.transparent,
 | 
						|
                    ],
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
          Positioned(
 | 
						|
            bottom: 0,
 | 
						|
            left: 0,
 | 
						|
            right: 0,
 | 
						|
            child: Column(
 | 
						|
              mainAxisAlignment: MainAxisAlignment.end,
 | 
						|
              crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
						|
              children: [
 | 
						|
                Wrap(
 | 
						|
                  spacing: 8,
 | 
						|
                  children: [
 | 
						|
                    if (item.fileMeta?['duration'] != null)
 | 
						|
                      Text(
 | 
						|
                        Duration(
 | 
						|
                          milliseconds:
 | 
						|
                              ((item.fileMeta?['duration'] as num) * 1000)
 | 
						|
                                  .toInt(),
 | 
						|
                        ).formatDuration(),
 | 
						|
                        style: TextStyle(
 | 
						|
                          color: Colors.white,
 | 
						|
                          shadows: [
 | 
						|
                            BoxShadow(
 | 
						|
                              color: Colors.black54,
 | 
						|
                              offset: Offset(1, 1),
 | 
						|
                              spreadRadius: 8,
 | 
						|
                              blurRadius: 8,
 | 
						|
                            ),
 | 
						|
                          ],
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                    if (item.fileMeta?['bit_rate'] != null)
 | 
						|
                      Text(
 | 
						|
                        '${int.parse(item.fileMeta?['bit_rate'] as String) ~/ 1000} Kbps',
 | 
						|
                        style: TextStyle(
 | 
						|
                          color: Colors.white,
 | 
						|
                          shadows: [
 | 
						|
                            BoxShadow(
 | 
						|
                              color: Colors.black54,
 | 
						|
                              offset: Offset(1, 1),
 | 
						|
                              spreadRadius: 8,
 | 
						|
                              blurRadius: 8,
 | 
						|
                            ),
 | 
						|
                          ],
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
                Text(
 | 
						|
                  item.name,
 | 
						|
                  maxLines: 1,
 | 
						|
                  overflow: TextOverflow.ellipsis,
 | 
						|
                  style: TextStyle(
 | 
						|
                    color: Colors.white,
 | 
						|
                    fontWeight: FontWeight.bold,
 | 
						|
                    shadows: [
 | 
						|
                      BoxShadow(
 | 
						|
                        color: Colors.black54,
 | 
						|
                        offset: Offset(1, 1),
 | 
						|
                        spreadRadius: 8,
 | 
						|
                        blurRadius: 8,
 | 
						|
                      ),
 | 
						|
                    ],
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ).padding(horizontal: 16, bottom: 12),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
      onTap: () {
 | 
						|
        open.value = true;
 | 
						|
      },
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class CloudImageWidget extends ConsumerWidget {
 | 
						|
  final String? fileId;
 | 
						|
  final SnCloudFile? file;
 | 
						|
  final BoxFit fit;
 | 
						|
  final double aspectRatio;
 | 
						|
  final String? blurHash;
 | 
						|
  const CloudImageWidget({
 | 
						|
    super.key,
 | 
						|
    this.fileId,
 | 
						|
    this.file,
 | 
						|
    this.aspectRatio = 1,
 | 
						|
    this.fit = BoxFit.cover,
 | 
						|
    this.blurHash,
 | 
						|
  });
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final serverUrl = ref.watch(serverUrlProvider);
 | 
						|
    final uri = '$serverUrl/drive/files/${file?.id ?? fileId}';
 | 
						|
 | 
						|
    return AspectRatio(
 | 
						|
      aspectRatio: aspectRatio,
 | 
						|
      child:
 | 
						|
          file != null
 | 
						|
              ? CloudFileWidget(item: file!, fit: fit)
 | 
						|
              : UniversalImage(uri: uri, blurHash: blurHash, fit: fit),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  static ImageProvider provider({
 | 
						|
    required String fileId,
 | 
						|
    required String serverUrl,
 | 
						|
    bool original = false,
 | 
						|
  }) {
 | 
						|
    final uri =
 | 
						|
        original
 | 
						|
            ? '$serverUrl/drive/files/$fileId?original=true'
 | 
						|
            : '$serverUrl/drive/files/$fileId';
 | 
						|
    return CachedNetworkImageProvider(uri);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class ProfilePictureWidget extends ConsumerWidget {
 | 
						|
  final String? fileId;
 | 
						|
  final SnCloudFile? file;
 | 
						|
  final double radius;
 | 
						|
  final double? borderRadius;
 | 
						|
  final IconData? fallbackIcon;
 | 
						|
  final Color? fallbackColor;
 | 
						|
  const ProfilePictureWidget({
 | 
						|
    super.key,
 | 
						|
    this.fileId,
 | 
						|
    this.file,
 | 
						|
    this.radius = 20,
 | 
						|
    this.borderRadius,
 | 
						|
    this.fallbackIcon,
 | 
						|
    this.fallbackColor,
 | 
						|
  });
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final serverUrl = ref.watch(serverUrlProvider);
 | 
						|
    final String? id = file?.id ?? fileId;
 | 
						|
 | 
						|
    final fallback =
 | 
						|
        Icon(
 | 
						|
          fallbackIcon ?? Symbols.account_circle,
 | 
						|
          size: radius,
 | 
						|
          color:
 | 
						|
              fallbackColor ?? Theme.of(context).colorScheme.onPrimaryContainer,
 | 
						|
        ).center();
 | 
						|
 | 
						|
    return ClipRRect(
 | 
						|
      borderRadius:
 | 
						|
          borderRadius == null
 | 
						|
              ? BorderRadius.all(Radius.circular(radius))
 | 
						|
              : BorderRadius.all(Radius.circular(borderRadius!)),
 | 
						|
      child: Container(
 | 
						|
        width: radius * 2,
 | 
						|
        height: radius * 2,
 | 
						|
        color: Theme.of(context).colorScheme.primaryContainer,
 | 
						|
        child:
 | 
						|
            id == null
 | 
						|
                ? fallback
 | 
						|
                : DataSavingGate(
 | 
						|
                  bypass: true,
 | 
						|
                  placeholder: fallback,
 | 
						|
                  content:
 | 
						|
                      () => UniversalImage(
 | 
						|
                        uri: '$serverUrl/drive/files/$id',
 | 
						|
                        fit: BoxFit.cover,
 | 
						|
                      ),
 | 
						|
                ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class SplitAvatarWidget extends ConsumerWidget {
 | 
						|
  final List<String?> filesId;
 | 
						|
  final double radius;
 | 
						|
  final IconData fallbackIcon;
 | 
						|
  final Color? fallbackColor;
 | 
						|
 | 
						|
  const SplitAvatarWidget({
 | 
						|
    super.key,
 | 
						|
    required this.filesId,
 | 
						|
    this.radius = 20,
 | 
						|
    this.fallbackIcon = Symbols.account_circle,
 | 
						|
    this.fallbackColor,
 | 
						|
  });
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    if (filesId.isEmpty) {
 | 
						|
      return ProfilePictureWidget(
 | 
						|
        fileId: null,
 | 
						|
        radius: radius,
 | 
						|
        fallbackIcon: fallbackIcon,
 | 
						|
        fallbackColor: fallbackColor,
 | 
						|
      );
 | 
						|
    }
 | 
						|
    if (filesId.length == 1) {
 | 
						|
      return ProfilePictureWidget(
 | 
						|
        fileId: filesId[0],
 | 
						|
        radius: radius,
 | 
						|
        fallbackIcon: fallbackIcon,
 | 
						|
        fallbackColor: fallbackColor,
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return ClipRRect(
 | 
						|
      borderRadius: BorderRadius.all(Radius.circular(radius)),
 | 
						|
      child: Container(
 | 
						|
        width: radius * 2,
 | 
						|
        height: radius * 2,
 | 
						|
        color: Theme.of(context).colorScheme.primaryContainer,
 | 
						|
        child: Stack(
 | 
						|
          children: [
 | 
						|
            if (filesId.length == 2)
 | 
						|
              Row(
 | 
						|
                children: [
 | 
						|
                  Expanded(
 | 
						|
                    child: _buildQuadrant(context, filesId[0], ref, radius),
 | 
						|
                  ),
 | 
						|
                  Expanded(
 | 
						|
                    child: _buildQuadrant(context, filesId[1], ref, radius),
 | 
						|
                  ),
 | 
						|
                ],
 | 
						|
              )
 | 
						|
            else if (filesId.length == 3)
 | 
						|
              Row(
 | 
						|
                children: [
 | 
						|
                  Column(
 | 
						|
                    children: [
 | 
						|
                      Expanded(
 | 
						|
                        child: _buildQuadrant(context, filesId[0], ref, radius),
 | 
						|
                      ),
 | 
						|
                      Expanded(
 | 
						|
                        child: _buildQuadrant(context, filesId[1], ref, radius),
 | 
						|
                      ),
 | 
						|
                    ],
 | 
						|
                  ),
 | 
						|
                  Expanded(
 | 
						|
                    child: _buildQuadrant(context, filesId[2], ref, radius),
 | 
						|
                  ),
 | 
						|
                ],
 | 
						|
              )
 | 
						|
            else
 | 
						|
              Column(
 | 
						|
                children: [
 | 
						|
                  Expanded(
 | 
						|
                    child: Row(
 | 
						|
                      children: [
 | 
						|
                        Expanded(
 | 
						|
                          child: _buildQuadrant(
 | 
						|
                            context,
 | 
						|
                            filesId[0],
 | 
						|
                            ref,
 | 
						|
                            radius,
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                        Expanded(
 | 
						|
                          child: _buildQuadrant(
 | 
						|
                            context,
 | 
						|
                            filesId[1],
 | 
						|
                            ref,
 | 
						|
                            radius,
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                  Expanded(
 | 
						|
                    child: Row(
 | 
						|
                      children: [
 | 
						|
                        Expanded(
 | 
						|
                          child: _buildQuadrant(
 | 
						|
                            context,
 | 
						|
                            filesId[2],
 | 
						|
                            ref,
 | 
						|
                            radius,
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                        Expanded(
 | 
						|
                          child:
 | 
						|
                              filesId.length > 4
 | 
						|
                                  ? Container(
 | 
						|
                                    color:
 | 
						|
                                        Theme.of(
 | 
						|
                                          context,
 | 
						|
                                        ).colorScheme.primaryContainer,
 | 
						|
                                    child: Center(
 | 
						|
                                      child: Text(
 | 
						|
                                        '+${filesId.length - 3}',
 | 
						|
                                        style: TextStyle(
 | 
						|
                                          fontSize: radius * 0.4,
 | 
						|
                                          color:
 | 
						|
                                              Theme.of(
 | 
						|
                                                context,
 | 
						|
                                              ).colorScheme.onPrimaryContainer,
 | 
						|
                                        ),
 | 
						|
                                      ),
 | 
						|
                                    ),
 | 
						|
                                  )
 | 
						|
                                  : _buildQuadrant(
 | 
						|
                                    context,
 | 
						|
                                    filesId[3],
 | 
						|
                                    ref,
 | 
						|
                                    radius,
 | 
						|
                                  ),
 | 
						|
                        ),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                ],
 | 
						|
              ),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Widget _buildQuadrant(
 | 
						|
    BuildContext context,
 | 
						|
    String? fileId,
 | 
						|
    WidgetRef ref,
 | 
						|
    double radius,
 | 
						|
  ) {
 | 
						|
    if (fileId == null) {
 | 
						|
      return Container(
 | 
						|
        width: radius,
 | 
						|
        height: radius,
 | 
						|
        color: Theme.of(context).colorScheme.primaryContainer,
 | 
						|
        child:
 | 
						|
            Icon(
 | 
						|
              fallbackIcon,
 | 
						|
              size: radius * 0.6,
 | 
						|
              color:
 | 
						|
                  fallbackColor ??
 | 
						|
                  Theme.of(context).colorScheme.onPrimaryContainer,
 | 
						|
            ).center(),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    final serverUrl = ref.watch(serverUrlProvider);
 | 
						|
    final uri = '$serverUrl/drive/files/$fileId';
 | 
						|
 | 
						|
    return SizedBox(
 | 
						|
      width: radius,
 | 
						|
      height: radius,
 | 
						|
      child: UniversalImage(uri: uri, fit: BoxFit.cover),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |