Compare commits

..

No commits in common. "3b1e91811791e5831ff84362d2d1732a633d0a21" and "2dc50d627eae65572dd3c7e85602480fc8c70ae9" have entirely different histories.

9 changed files with 218 additions and 381 deletions

View File

@ -17,7 +17,6 @@
android:label="Solian" android:label="Solian"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true"
android:requestLegacyExternalStorage="true"> android:requestLegacyExternalStorage="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

View File

@ -294,7 +294,6 @@
"addAttachmentFromCameraPhoto": "Take photo", "addAttachmentFromCameraPhoto": "Take photo",
"addAttachmentFromCameraVideo": "Take video", "addAttachmentFromCameraVideo": "Take video",
"addAttachmentFromRandomId": "Link via RID", "addAttachmentFromRandomId": "Link via RID",
"attachmentDetailInfo": "Attachment details",
"attachmentPastedImage": "Pasted Image", "attachmentPastedImage": "Pasted Image",
"attachmentInsertLink": "Insert Link", "attachmentInsertLink": "Insert Link",
"attachmentSetAsPostThumbnail": "Set as post thumbnail", "attachmentSetAsPostThumbnail": "Set as post thumbnail",

View File

@ -292,7 +292,6 @@
"addAttachmentFromCameraPhoto": "拍摄照片", "addAttachmentFromCameraPhoto": "拍摄照片",
"addAttachmentFromCameraVideo": "拍摄视频", "addAttachmentFromCameraVideo": "拍摄视频",
"addAttachmentFromRandomId": "通过访问 ID 链接", "addAttachmentFromRandomId": "通过访问 ID 链接",
"attachmentDetailInfo": "附件详细信息",
"attachmentPastedImage": "粘贴的图片", "attachmentPastedImage": "粘贴的图片",
"attachmentInsertLink": "插入连接", "attachmentInsertLink": "插入连接",
"attachmentSetAsPostThumbnail": "设置为帖子缩略图", "attachmentSetAsPostThumbnail": "设置为帖子缩略图",

View File

@ -217,48 +217,27 @@ class _ExploreScreenState extends State<ExploreScreen> {
hasReachedMax: _postCount != null && _posts.length >= _postCount!, hasReachedMax: _postCount != null && _posts.length >= _postCount!,
onFetchData: _fetchPosts, onFetchData: _fetchPosts,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
return Center( return GestureDetector(
child: Container( child: PostItem(
decoration: BoxDecoration( data: _posts[idx],
border: Border( maxWidth: 640,
left: BorderSide( onChanged: (data) {
color: Theme.of(context).dividerColor, setState(() => _posts[idx] = data);
width: 1 / MediaQuery.of(context).devicePixelRatio, },
), onDeleted: () {
right: BorderSide( _refreshPosts();
color: Theme.of(context).dividerColor, },
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
constraints: const BoxConstraints(maxWidth: 640),
child: Column(
children: [
GestureDetector(
child: PostItem(
data: _posts[idx],
maxWidth: 640,
onChanged: (data) {
setState(() => _posts[idx] = data);
},
onDeleted: () {
_refreshPosts();
},
),
onTap: () {
GoRouter.of(context).pushNamed(
'postDetail',
pathParameters: {'slug': _posts[idx].id.toString()},
extra: _posts[idx],
);
},
),
const Divider(height: 1),
],
),
), ),
onTap: () {
GoRouter.of(context).pushNamed(
'postDetail',
pathParameters: {'slug': _posts[idx].id.toString()},
extra: _posts[idx],
);
},
); );
}, },
separatorBuilder: (context, index) => const Divider(height: 1),
), ),
], ],
), ),

View File

@ -120,7 +120,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
subtitle: Text('settingsThemeMaterial3Description').tr(), subtitle: Text('settingsThemeMaterial3Description').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17), contentPadding: const EdgeInsets.only(left: 24, right: 17),
secondary: const Icon(Symbols.new_releases), secondary: const Icon(Symbols.new_releases),
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? true, value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_prefs.setBool( _prefs.setBool(

View File

@ -36,7 +36,7 @@ Future<ThemeData> createAppTheme(
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false; final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
return ThemeData( return ThemeData(
useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true), useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false),
colorScheme: colorScheme, colorScheme: colorScheme,
brightness: brightness, brightness: brightness,
iconTheme: IconThemeData( iconTheme: IconThemeData(
@ -52,15 +52,5 @@ Future<ThemeData> createAppTheme(
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary, foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
), ),
scaffoldBackgroundColor: Colors.transparent, scaffoldBackgroundColor: Colors.transparent,
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
TargetPlatform.iOS: ZoomPageTransitionsBuilder(),
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
TargetPlatform.linux: ZoomPageTransitionsBuilder(),
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
},
),
); );
} }

View File

@ -10,6 +10,7 @@ import 'package:surface/providers/experience.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/screens/account/profile_page.dart'; import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/types/account.dart'; import 'package:surface/types/account.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';

View File

@ -129,8 +129,6 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75); Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
bool _showDetail = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
@ -146,348 +144,218 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
onDismissed: () { onDismissed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
direction: DismissiblePageDismissDirection.none, direction: DismissiblePageDismissDirection.down,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
isFullScreen: true, isFullScreen: true,
child: GestureDetector( child: Scaffold(
behavior: HitTestBehavior.translucent, body: Stack(
child: Scaffold( children: [
body: Stack( Builder(builder: (context) {
children: [ if (widget.data.length == 1) {
Builder(builder: (context) { final heroTag = widget.heroTags?.first ?? uuid.v4();
if (widget.data.length == 1) { return Hero(
final heroTag = widget.heroTags?.first ?? uuid.v4(); tag: 'attachment-${widget.data.first.rid}-$heroTag',
return Hero( child: PhotoView(
tag: 'attachment-${widget.data.first.rid}-$heroTag', key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
child: PhotoView( backgroundDecoration: BoxDecoration(color: Colors.transparent),
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'), imageProvider: UniversalImage.provider(
backgroundDecoration: BoxDecoration(color: Colors.transparent), sn.getAttachmentUrl(widget.data.first.rid),
imageProvider: UniversalImage.provider( ),
sn.getAttachmentUrl(widget.data.first.rid), ),
), );
}
return PhotoViewGallery.builder(
pageController: _pageController,
scrollPhysics: const BouncingScrollPhysics(),
builder: (context, idx) {
final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
return PhotoViewGalleryPageOptions(
imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
),
heroAttributes: PhotoViewHeroAttributes(
tag: 'attachment-${widget.data.first.rid}-$heroTag',
), ),
); );
} },
itemCount: widget.data.length,
return PhotoViewGallery.builder( loadingBuilder: (context, event) => Center(
pageController: _pageController, child: SizedBox(
scrollPhysics: const BouncingScrollPhysics(), width: 20.0,
builder: (context, idx) { height: 20.0,
final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4(); child: CircularProgressIndicator(
return PhotoViewGalleryPageOptions( value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1),
imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
),
heroAttributes: PhotoViewHeroAttributes(
tag: 'attachment-${widget.data.first.rid}-$heroTag',
),
);
},
itemCount: widget.data.length,
loadingBuilder: (context, event) => Center(
child: SizedBox(
width: 20.0,
height: 20.0,
child: CircularProgressIndicator(
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1),
),
), ),
), ),
backgroundDecoration: BoxDecoration(color: Colors.transparent), ),
); backgroundDecoration: BoxDecoration(color: Colors.transparent),
}), );
Align( }),
alignment: Alignment.bottomCenter, Align(
child: IgnorePointer( alignment: Alignment.bottomCenter,
child: Container( child: IgnorePointer(
height: 300, child: Container(
decoration: BoxDecoration( height: 300,
gradient: LinearGradient( decoration: BoxDecoration(
begin: Alignment.bottomCenter, gradient: LinearGradient(
end: Alignment.topCenter, begin: Alignment.bottomCenter,
colors: [ end: Alignment.topCenter,
Theme.of(context).colorScheme.surface, colors: [
Colors.transparent, Theme.of(context).colorScheme.surface,
], Colors.transparent,
), ],
), ),
), ),
), ),
), ),
Positioned( ),
left: 16, Positioned(
right: 16, left: 16,
bottom: 16 + MediaQuery.of(context).padding.bottom, right: 16,
child: Material( bottom: 16 + MediaQuery.of(context).padding.bottom,
color: Colors.transparent, child: Material(
child: Builder(builder: (context) { color: Colors.transparent,
final ud = context.read<UserDirectoryProvider>(); child: Builder(builder: (context) {
final item = widget.data.elementAt( final ud = context.read<UserDirectoryProvider>();
widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0, final item = widget.data.elementAt(
); widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
final account = ud.getAccountFromCache(item.accountId); );
final account = ud.getAccountFromCache(item.accountId);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (item.accountId > 0) if (item.accountId > 0)
Row( Row(
children: [ children: [
IgnorePointer( IgnorePointer(
child: AccountImage( child: AccountImage(
content: account?.avatar, content: account?.avatar,
radius: 19, radius: 19,
),
), ),
const Gap(8),
Expanded(
child: IgnorePointer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'attachmentUploadBy'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
Text(
account?.nick ?? 'unknown'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
),
if (widget.data.length > 1)
IgnorePointer(
child: Text(
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}',
style: GoogleFonts.robotoMono(fontSize: 13),
).padding(right: 8),
),
InkWell(
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: _isDownloading
? null
: () =>
_saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
child: Container(
padding: const EdgeInsets.all(6),
child: !_isDownloading
? !_isCompletedDownload
? const Icon(Symbols.save_alt)
: const Icon(Symbols.download_done)
: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _progressOfDownload,
strokeWidth: 3,
),
),
),
),
],
),
const Gap(4),
IgnorePointer(
child: Text(
item.alt,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
), ),
const Gap(8),
Expanded(
child: IgnorePointer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'attachmentUploadBy'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
Text(
account?.nick ?? 'unknown'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
),
if (widget.data.length > 1)
IgnorePointer(
child: Text(
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}',
style: GoogleFonts.robotoMono(fontSize: 13),
).padding(right: 8),
),
InkWell(
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: _isDownloading
? null
: () => _saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
child: Container(
padding: const EdgeInsets.all(6),
child: !_isDownloading
? !_isCompletedDownload
? const Icon(Symbols.save_alt)
: const Icon(Symbols.download_done)
: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _progressOfDownload,
strokeWidth: 3,
),
),
),
),
],
),
const Gap(4),
IgnorePointer(
child: Text(
item.alt,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
), ),
), ),
const Gap(2), ),
IgnorePointer( const Gap(2),
child: Wrap( IgnorePointer(
spacing: 6, child: Wrap(
children: [ spacing: 6,
if (item.metadata['exif'] == null) children: [
Text( if (item.metadata['exif'] == null)
'#${item.rid}',
style: metaTextStyle,
),
if (item.metadata['exif']?['Model'] != null)
Text(
'attachmentShotOn'.tr(args: [
item.metadata['exif']?['Model'],
]),
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['ISO'] != null)
Text(
'ISO${item.metadata['exif']?['ISO']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Aperture'] != null)
Text(
'f/${item.metadata['exif']?['Aperture']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Megapixels'] != null &&
item.metadata['exif']?['Model'] != null)
Text(
'${item.metadata['exif']?['Megapixels']}MP',
style: metaTextStyle,
)
else
Text(
item.size.formatBytes(),
style: metaTextStyle,
),
if (item.metadata['width'] != null && item.metadata['height'] != null)
Text(
'${item.metadata['width']}x${item.metadata['height']}',
style: metaTextStyle,
),
if (item.metadata['ratio'] != null)
Text(
(item.metadata['ratio'] as num).toStringAsFixed(2),
style: metaTextStyle,
),
Text( Text(
item.mimetype, '#${item.rid}',
style: metaTextStyle, style: metaTextStyle,
), ),
], if (item.metadata['exif']?['Model'] != null)
), Text(
), 'attachmentShotOn'.tr(args: [
], item.metadata['exif']?['Model'],
); ]),
}), style: metaTextStyle,
), ).padding(right: 2),
), if (item.metadata['exif']?['ISO'] != null)
], Text(
), 'ISO${item.metadata['exif']?['ISO']}',
), style: metaTextStyle,
onVerticalDragUpdate: (details) { ).padding(right: 2),
if (_showDetail) return; if (item.metadata['exif']?['Aperture'] != null)
if (details.delta.dy < 0) { Text(
_showDetail = true; 'f/${item.metadata['exif']?['Aperture']}',
showModalBottomSheet( style: metaTextStyle,
context: context, ).padding(right: 2),
builder: (context) => _AttachmentZoomDetailPopup( if (item.metadata['exif']?['Megapixels'] != null && item.metadata['exif']?['Model'] != null)
data: widget.data.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0), Text(
), '${item.metadata['exif']?['Megapixels']}MP',
).then((_) { style: metaTextStyle,
_showDetail = false; )
}); else
} Text(
}, item.size.formatBytes(),
onTap: () { style: metaTextStyle,
Navigator.of(context).pop(); ),
}, if (item.metadata['width'] != null && item.metadata['height'] != null)
), Text(
); '${item.metadata['width']}x${item.metadata['height']}',
} style: metaTextStyle,
} ),
if (item.metadata['ratio'] != null)
class _AttachmentZoomDetailPopup extends StatelessWidget { Text(
final SnAttachment data; (item.metadata['ratio'] as num).toStringAsFixed(2),
style: metaTextStyle,
const _AttachmentZoomDetailPopup({required this.data}); ),
Text(
@override item.mimetype,
Widget build(BuildContext context) { style: metaTextStyle,
final ud = context.read<UserDirectoryProvider>();
final account = ud.getAccountFromCache(data.accountId);
const tableGap = TableRow(
children: [
TableCell(child: SizedBox(height: 16)),
TableCell(child: SizedBox(height: 16)),
],
);
return SizedBox(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.info, size: 24),
const Gap(16),
Text('attachmentDetailInfo').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Expanded(
child: Table(
columnWidths: {
0: IntrinsicColumnWidth(),
1: FlexColumnWidth(),
},
children: [
TableRow(
children: [
TableCell(
child: Text('attachmentUploadBy').tr().padding(right: 16),
),
TableCell(
child: Row(
children: [
if (data.accountId > 0)
AccountImage(
content: account?.avatar,
radius: 8,
), ),
const Gap(8), ],
Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()), ),
const Gap(8),
Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75),
],
), ),
),
],
),
tableGap,
TableRow(
children: [
TableCell(child: Text('Mimetype').padding(right: 16)),
TableCell(child: Text(data.mimetype)),
],
),
TableRow(
children: [
TableCell(child: Text('Size').padding(right: 16)),
TableCell(
child: Row(
children: [
Text(data.size.formatBytes()),
const Gap(12),
Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75),
],
)),
],
),
TableRow(
children: [
TableCell(child: Text('Name').padding(right: 16)),
TableCell(child: Text(data.name)),
],
),
if (data.hash.isNotEmpty)
TableRow(
children: [
TableCell(child: Text('Hash').padding(right: 16)),
TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)),
], ],
), );
tableGap, }),
...(data.metadata['exif']?.keys.map((k) => TableRow( ),
children: [ ),
TableCell(child: Text(k).padding(right: 16)), ],
TableCell(child: Text(data.metadata['exif'][k].toString())), ),
],
)) ??
[]),
],
).padding(horizontal: 20, vertical: 8),
),
],
), ),
); );
} }

View File

@ -18,7 +18,9 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context)); context
.read<NavigationProvider>()
.autoDetectIndex(GoRouter.maybeOf(context));
}); });
} }
@ -29,11 +31,11 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
return ListenableBuilder( return ListenableBuilder(
listenable: nav, listenable: nav,
builder: (context, _) { builder: (context, _) {
final destinations = nav.destinations.where((ele) => ele.isPinned).toList(); final destinations =
nav.destinations.where((ele) => ele.isPinned).toList();
return NavigationRail( return NavigationRail(
selectedIndex: selectedIndex: nav.currentIndex,
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
destinations: [ destinations: [
...destinations.where((ele) => ele.isPinned).map((ele) { ...destinations.where((ele) => ele.isPinned).map((ele) {
return NavigationRailDestination( return NavigationRailDestination(