Files
eh_downloader_flutter/lib/dialog/gallery_share_page.dart
2024-08-28 10:07:12 +00:00

420 lines
14 KiB
Dart

import 'dart:ui';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_datetime_picker_plus/flutter_datetime_picker_plus.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import '../api/token.dart';
import '../globals.dart';
import '../main.dart';
import '../platform/media_query.dart';
import '../utils.dart';
import '../utils/clipboard.dart';
final _log = Logger("GallerySharePage");
Future<void> _change(
String token, DateTime? expired, AppLocalizations i18n) async {
try {
final t = (await api.updateShareGallery(token,
expired: expired?.millisecondsSinceEpoch))
.unwrap();
listener.tryEmit("gallery_share_token_changed", t);
} catch (e, stack) {
String errmsg = "${i18n.failedChangeExpireTime}$e";
if (e is (int, String)) {
_log.warning("Failed to change expire time: $e");
} else {
_log.severe("Failed to change expire time: $e\n$stack");
}
final snack = SnackBar(content: Text(errmsg));
rootScaffoldMessengerKey.currentState?.showSnackBar(snack);
}
}
Future<void> _add(int gid, DateTime? expired, AppLocalizations i18n) async {
try {
final t =
(await api.shareGallery(gid, expired: expired?.millisecondsSinceEpoch))
.unwrap();
listener.tryEmit("gallery_share_token_added", t);
} catch (e, stack) {
String errmsg = "${i18n.failedShareGallery}$e";
if (e is (int, String)) {
_log.warning("Failed to share gallery: $e");
} else {
_log.severe("Failed to share gallery: $e\n$stack");
}
final snack = SnackBar(content: Text(errmsg));
rootScaffoldMessengerKey.currentState?.showSnackBar(snack);
}
}
class _ChangeDialog extends StatefulWidget {
const _ChangeDialog(this.gid, {this.token});
final int gid;
final String? token;
@override
State<_ChangeDialog> createState() => _ChangeDialogState();
}
enum _ExpireDuration {
day,
week,
month,
never,
custom;
DateTime? expiredTime() {
switch (this) {
case _ExpireDuration.day:
return DateTime.now().add(const Duration(days: 1));
case _ExpireDuration.week:
return DateTime.now().add(const Duration(days: 7));
case _ExpireDuration.month:
return DateTime.now().add(const Duration(days: 30));
default:
return null;
}
}
String localText(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
switch (this) {
case _ExpireDuration.custom:
return i18n.custom;
case _ExpireDuration.day:
return i18n.oneDayAfter;
case _ExpireDuration.week:
return i18n.oneWeekAfter;
case _ExpireDuration.month:
return i18n.oneMonthAfter;
case _ExpireDuration.never:
return i18n.never;
}
}
}
class _ChangeDialogState extends State<_ChangeDialog> {
final _formKey = GlobalKey<FormState>();
DateTime _expired = DateTime.now();
_ExpireDuration _dur = _ExpireDuration.never;
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
return AlertDialog(
title:
Text(widget.token != null ? i18n.editExpireTime : i18n.shareGallery),
content: Form(
key: _formKey,
child: Column(mainAxisSize: MainAxisSize.min, children: [
DropdownButtonFormField<_ExpireDuration>(
items: _ExpireDuration.values
.map((e) => DropdownMenuItem(
value: e, child: Text(e.localText(context))))
.toList(),
value: _dur,
onChanged: (dur) {
if (dur != null) {
setState(() {
_dur = dur;
});
}
if (dur == _ExpireDuration.custom) {
DatePicker.showDateTimePicker(context, onConfirm: (e) {
setState(() {
_expired = e;
});
},
locale: MainApp.of(context).lang.toLocaleType(),
minTime: DateTime.now());
}
},
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.expireTime,
)),
]),
),
actions: [
TextButton(
onPressed: _dur != _ExpireDuration.custom ||
_dur == _ExpireDuration.custom &&
_expired.isAfter(DateTime.now())
? () {
final expired = _dur != _ExpireDuration.custom
? _dur.expiredTime()
: _expired;
if (widget.token != null) {
_change(widget.token!, expired, i18n);
} else {
_add(widget.gid, expired, i18n);
}
context.pop();
}
: null,
child: Text(widget.token != null ? i18n.edit : i18n.share)),
TextButton(
onPressed: () {
context.pop();
},
child: Text(i18n.cancel)),
],
);
}
}
class GallerySharePage extends StatefulWidget {
const GallerySharePage(this.gid, {super.key});
final int gid;
static const routeName = "/dialog/gallery/share/:gid";
@override
State<GallerySharePage> createState() => _GallerySharePage();
}
class _GallerySharePage extends State<GallerySharePage> {
List<SharedTokenWithUrl>? _lists;
CancelToken? _cancel;
bool _isLoading = false;
Object? _error;
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
Future<void> _fetchList() async {
_cancel = CancelToken();
_isLoading = true;
try {
_lists = (await api.listShareGalleries(gid: widget.gid, cancel: _cancel))
.unwrap();
if (!_cancel!.isCancelled) {
setState(() {
_isLoading = false;
});
}
} catch (e) {
if (!_cancel!.isCancelled) {
_log.severe("Failed to load gallery shared list ${widget.gid}:", e);
setState(() {
_error = e;
_isLoading = false;
});
}
}
}
void onTokenChanged(dynamic arg) {
if (_lists == null) return;
final t = arg as SharedTokenWithUrl;
final ind = _lists!.indexWhere((a) => a.token.id == t.token.id);
if (ind != -1) {
setState(() {
_lists![ind] = t;
});
}
}
void onTokenAdded(dynamic arg) {
if (_lists == null) return;
final t = arg as SharedTokenWithUrl;
final g = t.token.info as GallerySharedTokenInfo;
if (g.gid == widget.gid) {
setState(() {
_lists!.add(t);
});
}
}
@override
void initState() {
listener.on("gallery_share_token_changed", onTokenChanged);
listener.on("gallery_share_token_added", onTokenAdded);
super.initState();
}
@override
void dispose() {
_cancel?.cancel();
listener.removeEventListener("gallery_share_token_changed", onTokenChanged);
listener.removeEventListener("gallery_share_token_added", onTokenAdded);
super.dispose();
}
Widget _buildView(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
final p = Theme.of(context).colorScheme.primary;
final s = TextStyle(color: p);
return CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: <Widget>[
SliverToBoxAdapter(
child: Stack(
alignment: Alignment.center,
children: [
Text(
i18n.shareGallery,
style: Theme.of(context).textTheme.headlineSmall,
),
Align(
alignment: Alignment.centerRight,
child: IconButton(
onPressed: () => context.canPop()
? context.pop()
: context.go("/gallery/${widget.gid}"),
icon: const Icon(Icons.close),
)),
],
)),
SliverToBoxAdapter(
child: Row(children: [
Expanded(
child: Text(i18n.token, textAlign: TextAlign.center, style: s)),
Expanded(
child: Text(i18n.expireTime,
textAlign: TextAlign.center, style: s)),
SizedBox(
width: 120,
child:
Text(i18n.action, textAlign: TextAlign.center, style: s)),
])),
SliverList.builder(
itemBuilder: (context, index) {
final item = _lists![index];
return Row(children: [
Expanded(
child: SelectableText(item.token.token,
textAlign: TextAlign.center)),
Expanded(
child: SelectableText(
item.token.expired == null
? i18n.never
: DateFormat.yMd(MainApp.of(context)
.lang
.toLocale()
.toString())
.add_jms()
.format(item.token.expired!.toLocal()),
textAlign: TextAlign.center),
),
SizedBox(
width: 120,
child: Row(children: [
IconButton.filled(
onPressed: () {
copyTextToClipboard(item.url);
},
icon: const Icon(Icons.link)),
IconButton.filled(
onPressed: () => showDialog(
context: context,
builder: (context) => _ChangeDialog(widget.gid,
token: item.token.token)),
icon: const Icon(Icons.edit)),
IconButton.filled(
onPressed: () => showDialog(
context: context,
builder: (context) {
return AlertDialog(title: Text(i18n.delete));
}),
icon: const Icon(Icons.delete)),
]),
),
]);
},
itemCount: _lists!.length),
]);
}
Widget _buildRefreshIcon(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
return IconButton(
onPressed: () {
_refreshIndicatorKey.currentState?.show();
},
tooltip: i18n.refresh,
icon: const Icon(Icons.refresh));
}
Widget _buildAddIcon(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
return IconButton(
onPressed: () => showDialog(
context: context, builder: (context) => _ChangeDialog(widget.gid)),
tooltip: i18n.create,
icon: const Icon(Icons.add));
}
Widget _buildIconList(BuildContext context) {
return Row(children: [
isDesktop || (kIsWeb && pointerIsMouse)
? _buildRefreshIcon(context)
: Container(),
_buildAddIcon(context),
]);
}
@override
Widget build(BuildContext context) {
tryInitApi(context);
final isLoading = _lists == null && _error == null;
final i18n = AppLocalizations.of(context)!;
final size = MediaQuery.of(context).size;
final maxWidth = size.width;
if (isLoading && !_isLoading) _fetchList();
return Container(
padding: maxWidth < 400
? const EdgeInsets.symmetric(vertical: 20, horizontal: 10)
: const EdgeInsets.all(20),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(10)),
width: maxWidth < 810 ? null : 800,
child: isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
? SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Error: $_error"),
ElevatedButton.icon(
onPressed: () {
_fetchList();
setState(() {
_error = null;
});
},
icon: const Icon(Icons.refresh),
label: Text(i18n.retry)),
],
))
: Stack(
children: <Widget>[
RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: () async {
return await _fetchList();
},
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.trackpad,
},
),
child: _buildView(context))),
Positioned(
bottom: size.height / 10,
right: size.width / 10,
child: _buildIconList(context))
],
),
);
}
}