mirror of
https://github.com/lifegpc/eh_downloader_flutter.git
synced 2026-07-08 01:30:28 +08:00
Add share token manage panel
This commit is contained in:
419
lib/dialog/gallery_share_page.dart
Normal file
419
lib/dialog/gallery_share_page.dart
Normal file
@@ -0,0 +1,419 @@
|
||||
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))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user