205 lines
8.1 KiB
Dart
205 lines
8.1 KiB
Dart
import 'package:easy_localization/easy_localization.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/network.dart';
|
|
import 'package:island/widgets/content/cloud_files.dart';
|
|
import 'package:island/widgets/content/sheet.dart';
|
|
import 'package:material_symbols_icons/symbols.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
|
import 'package:styled_widget/styled_widget.dart';
|
|
import 'package:url_launcher/url_launcher_string.dart';
|
|
|
|
part 'compose_link_attachments.g.dart';
|
|
|
|
@riverpod
|
|
class CloudFileListNotifier extends _$CloudFileListNotifier
|
|
with CursorPagingNotifierMixin<SnCloudFile> {
|
|
@override
|
|
Future<CursorPagingData<SnCloudFile>> build() => fetch(cursor: null);
|
|
|
|
@override
|
|
Future<CursorPagingData<SnCloudFile>> fetch({required String? cursor}) async {
|
|
final client = ref.read(apiClientProvider);
|
|
final offset = cursor == null ? 0 : int.parse(cursor);
|
|
final take = 20;
|
|
|
|
final queryParameters = {'offset': offset, 'take': take};
|
|
|
|
final response = await client.get(
|
|
'/drive/files/me',
|
|
queryParameters: queryParameters,
|
|
);
|
|
|
|
final List<SnCloudFile> items =
|
|
(response.data as List)
|
|
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
|
.toList();
|
|
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
|
|
|
final hasMore = offset + items.length < total;
|
|
final nextCursor = hasMore ? (offset + items.length).toString() : null;
|
|
|
|
return CursorPagingData(
|
|
items: items,
|
|
hasMore: hasMore,
|
|
nextCursor: nextCursor,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ComposeLinkAttachment extends HookConsumerWidget {
|
|
const ComposeLinkAttachment({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final idController = useTextEditingController();
|
|
final errorMessage = useState<String?>(null);
|
|
|
|
return SheetScaffold(
|
|
heightFactor: 0.6,
|
|
titleText: 'linkAttachment'.tr(),
|
|
child: DefaultTabController(
|
|
length: 2,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
TabBar(
|
|
tabs: [
|
|
Tab(text: 'attachmentsRecentUploads'.tr()),
|
|
Tab(text: 'attachmentsManualInput'.tr()),
|
|
],
|
|
),
|
|
Expanded(
|
|
child: TabBarView(
|
|
children: [
|
|
PagingHelperView(
|
|
provider: cloudFileListNotifierProvider,
|
|
futureRefreshable: cloudFileListNotifierProvider.future,
|
|
notifierRefreshable: cloudFileListNotifierProvider.notifier,
|
|
contentBuilder:
|
|
(data, widgetCount, endItemView) => ListView.builder(
|
|
padding: EdgeInsets.only(top: 8),
|
|
itemCount: widgetCount,
|
|
itemBuilder: (context, index) {
|
|
if (index == widgetCount - 1) {
|
|
return endItemView;
|
|
}
|
|
|
|
final item = data.items[index];
|
|
final itemType =
|
|
item.mimeType?.split('/').firstOrNull;
|
|
return ListTile(
|
|
leading: ClipRRect(
|
|
borderRadius: const BorderRadius.all(
|
|
Radius.circular(8),
|
|
),
|
|
child: SizedBox(
|
|
height: 48,
|
|
width: 48,
|
|
child: switch (itemType) {
|
|
'image' => CloudImageWidget(file: item),
|
|
'audio' =>
|
|
const Icon(
|
|
Symbols.audio_file,
|
|
fill: 1,
|
|
).center(),
|
|
'video' =>
|
|
const Icon(
|
|
Symbols.video_file,
|
|
fill: 1,
|
|
).center(),
|
|
_ =>
|
|
const Icon(
|
|
Symbols.body_system,
|
|
fill: 1,
|
|
).center(),
|
|
},
|
|
),
|
|
),
|
|
title:
|
|
item.name.isEmpty
|
|
? Text('untitled').tr().italic()
|
|
: Text(item.name),
|
|
onTap: () {
|
|
Navigator.pop(context, item);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
TextField(
|
|
controller: idController,
|
|
decoration: InputDecoration(
|
|
labelText: 'fileId'.tr(),
|
|
helperText: 'fileIdHint'.tr(),
|
|
helperMaxLines: 3,
|
|
errorText: errorMessage.value,
|
|
border: OutlineInputBorder(),
|
|
),
|
|
onTapOutside:
|
|
(_) =>
|
|
FocusManager.instance.primaryFocus?.unfocus(),
|
|
),
|
|
const Gap(16),
|
|
InkWell(
|
|
child: Text(
|
|
'fileIdLinkHint',
|
|
).tr().fontSize(13).opacity(0.85),
|
|
onTap: () {
|
|
launchUrlString('https://fs.solian.app');
|
|
},
|
|
).padding(horizontal: 14),
|
|
const Gap(16),
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: TextButton.icon(
|
|
icon: const Icon(Symbols.add),
|
|
label: Text('add'.tr()),
|
|
onPressed: () async {
|
|
final fileId = idController.text.trim();
|
|
if (fileId.isEmpty) {
|
|
errorMessage.value = 'fileIdCannotBeEmpty'.tr();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final client = ref.read(apiClientProvider);
|
|
final response = await client.get(
|
|
'/drive/files/$fileId/info',
|
|
);
|
|
final SnCloudFile cloudFile =
|
|
SnCloudFile.fromJson(response.data);
|
|
|
|
if (context.mounted) {
|
|
Navigator.of(context).pop(cloudFile);
|
|
}
|
|
} catch (e) {
|
|
errorMessage.value = 'failedToFetchFile'.tr(
|
|
args: [e.toString()],
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
],
|
|
).padding(horizontal: 24, vertical: 24),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|