Compare commits

...

6 Commits

Author SHA1 Message Date
654a71e852 🚀 Launch 2.0.0+2 2024-11-14 00:30:46 +08:00
455ffcac19 Expanded drawer nav 2024-11-14 00:20:59 +08:00
9c8dad0176 Drawer navigation 2024-11-14 00:08:09 +08:00
2c6b1feca6 🐛 Fix login didn't request factor code correctly 2024-11-13 22:39:51 +08:00
af044a86bc Quoted (repost) post 2024-11-13 22:05:40 +08:00
4884d04a51 Sensitive content blur 2024-11-13 21:36:28 +08:00
26 changed files with 794 additions and 270 deletions

13
.roadsignrc Normal file
View File

@@ -0,0 +1,13 @@
{
"sync": {
"region": "solian-next",
"configPath": "roadsign.toml"
},
"deployments": [
{
"region": "solian-next",
"site": "solian-next-web",
"path": "build/web"
}
]
}

View File

@@ -15,6 +15,8 @@
"screenAccountPublisherEdit": "Edit Publisher", "screenAccountPublisherEdit": "Edit Publisher",
"screenAccountProfileEdit": "Edit Profile", "screenAccountProfileEdit": "Edit Profile",
"screenSettings": "Settings", "screenSettings": "Settings",
"screenAlbum": "Album",
"screenChat": "Chat",
"dialogOkay": "Okay", "dialogOkay": "Okay",
"dialogCancel": "Cancel", "dialogCancel": "Cancel",
"dialogConfirm": "Confirm", "dialogConfirm": "Confirm",
@@ -95,9 +97,9 @@
"postReact": "React", "postReact": "React",
"postReactions": "Reactions of Post", "postReactions": "Reactions of Post",
"postReactionPoints": { "postReactionPoints": {
"zero": "{}pt", "zero": "{} pt",
"one": "{}pt", "one": "{} pt",
"other": "{}pts" "other": "{} pts"
}, },
"postReactCompleted": "Reaction has been added.", "postReactCompleted": "Reaction has been added.",
"postReactUncompleted": "Reaction has been removed.", "postReactUncompleted": "Reaction has been removed.",
@@ -125,5 +127,9 @@
"settingsNetworkServerResetDescription": "Reset to the official server address of Solar Network.", "settingsNetworkServerResetDescription": "Reset to the official server address of Solar Network.",
"settingsNetworkServerPreset": "Present HyperNet Server", "settingsNetworkServerPreset": "Present HyperNet Server",
"settingsNetworkServerPresetDescription": "You can choose one of our preset HyperNet server addresses from the list on the right.", "settingsNetworkServerPresetDescription": "You can choose one of our preset HyperNet server addresses from the list on the right.",
"settingsNetworkServerSaved": "Server address saved." "settingsNetworkServerSaved": "Server address saved.",
"sensitiveContent": "Sensitive Content",
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
"sensitiveContentReveal": "Reveal"
} }

View File

@@ -15,6 +15,8 @@
"screenAccountPublisherEdit": "编辑发布者", "screenAccountPublisherEdit": "编辑发布者",
"screenAccountProfileEdit": "编辑资料", "screenAccountProfileEdit": "编辑资料",
"screenSettings": "设置", "screenSettings": "设置",
"screenAlbum": "相册",
"screenChat": "聊天",
"dialogOkay": "好的", "dialogOkay": "好的",
"dialogCancel": "取消", "dialogCancel": "取消",
"dialogConfirm": "确认", "dialogConfirm": "确认",
@@ -125,5 +127,9 @@
"settingsNetworkServerResetDescription": "重设为 Solar Network 的服务器地址。", "settingsNetworkServerResetDescription": "重设为 Solar Network 的服务器地址。",
"settingsNetworkServerPreset": "预设的 HyperNet 服务器", "settingsNetworkServerPreset": "预设的 HyperNet 服务器",
"settingsNetworkServerPresetDescription": "你可以在旁边的列表中选择我们提供的预设 HyperNet 服务器地址。", "settingsNetworkServerPresetDescription": "你可以在旁边的列表中选择我们提供的预设 HyperNet 服务器地址。",
"settingsNetworkServerSaved": "服务器地址已保存。" "settingsNetworkServerSaved": "服务器地址已保存。",
"sensitiveContent": "敏感内容",
"sensitiveContentCollapsed": "敏感内容已折叠。",
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
"sensitiveContentReveal": "显示内容"
} }

View File

@@ -161,7 +161,6 @@
64FBE78F9C282712818D6D95 /* Pods-RunnerTests.release.xcconfig */, 64FBE78F9C282712818D6D95 /* Pods-RunnerTests.release.xcconfig */,
96081771773FA019A97CCC3F /* Pods-RunnerTests.profile.xcconfig */, 96081771773FA019A97CCC3F /* Pods-RunnerTests.profile.xcconfig */,
); );
name = Pods;
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -474,6 +473,7 @@
DEVELOPMENT_TEAM = W7HPZ53V6B; DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -657,6 +657,7 @@
DEVELOPMENT_TEAM = W7HPZ53V6B; DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -680,6 +681,7 @@
DEVELOPMENT_TEAM = W7HPZ53V6B; DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",

View File

@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Surface</string> <string>Solian</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>surface</string> <string>Solian</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>

View File

@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:responsive_framework/responsive_framework.dart';
import 'package:surface/providers/navigation.dart';
import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/theme.dart'; import 'package:surface/providers/theme.dart';
@@ -39,6 +40,7 @@ class SolianApp extends StatelessWidget {
providers: [ providers: [
Provider(create: (_) => SnNetworkProvider()), Provider(create: (_) => SnNetworkProvider()),
Provider(create: (ctx) => SnAttachmentProvider(ctx)), Provider(create: (ctx) => SnAttachmentProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => NavigationProvider()),
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)), ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
ChangeNotifierProvider(create: (_) => ThemeProvider()), ChangeNotifierProvider(create: (_) => ThemeProvider()),
], ],
@@ -59,6 +61,7 @@ class AppMainContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
context.read<NavigationProvider>();
context.read<UserProvider>(); context.read<UserProvider>();
final th = context.watch<ThemeProvider>(); final th = context.watch<ThemeProvider>();

View File

@@ -0,0 +1,112 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AppNavDestination {
final String label;
final String screen;
final Widget icon;
final bool isPinned;
const AppNavDestination({
required this.label,
required this.screen,
required this.icon,
this.isPinned = false,
});
}
class NavigationProvider extends ChangeNotifier {
int? _currentIndex;
int? get currentIndex => _currentIndex;
static const List<AppNavDestination> kAllDestination = [
AppNavDestination(
icon: Icon(Symbols.home, weight: 400, opticalSize: 20),
screen: 'home',
label: 'screenHome',
),
AppNavDestination(
icon: Icon(Symbols.explore, weight: 400, opticalSize: 20),
screen: 'explore',
label: 'screenExplore',
),
AppNavDestination(
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
screen: 'account',
label: 'screenAccount',
),
AppNavDestination(
icon: Icon(Symbols.album, weight: 400, opticalSize: 20),
screen: 'album',
label: 'screenAlbum',
),
AppNavDestination(
icon: Icon(Symbols.chat, weight: 400, opticalSize: 20),
screen: 'chat',
label: 'screenChat',
),
];
static const List<String> kDefaultPinnedDestination = [
'home',
'explore',
'account'
];
List<AppNavDestination> destinations = [];
int get pinnedDestinationCount =>
destinations.where((ele) => ele.isPinned).length;
NavigationProvider() {
buildDestinations(kDefaultPinnedDestination);
SharedPreferences.getInstance().then((prefs) {
final pinned = prefs.getStringList("app_pinned_navigation");
if (pinned != null) buildDestinations(pinned);
});
}
void buildDestinations(List<String> pinned) {
destinations = kAllDestination
.map(
(ele) => AppNavDestination(
label: ele.label,
screen: ele.screen,
icon: ele.icon,
isPinned: pinned.contains(ele.screen),
),
)
.toList();
notifyListeners();
}
int getIndexInRange(int min, int max) {
return math.max(min, math.min(_currentIndex ?? 0, max));
}
bool isIndexInRange(int min, int max) {
return _currentIndex != null &&
_currentIndex! >= min &&
_currentIndex! < max;
}
void autoDetectIndex(GoRouter? state) {
if (state == null) return;
final idx = destinations.indexWhere(
(ele) =>
ele.screen ==
state.routerDelegate.currentConfiguration.last.route.name,
);
_currentIndex = idx == -1 ? null : idx;
notifyListeners();
}
void setIndex(int idx) {
_currentIndex = idx;
notifyListeners();
}
}

View File

@@ -4,8 +4,10 @@ import 'package:surface/screens/account/profile_edit.dart';
import 'package:surface/screens/account/publishers/publisher_edit.dart'; import 'package:surface/screens/account/publishers/publisher_edit.dart';
import 'package:surface/screens/account/publishers/publisher_new.dart'; import 'package:surface/screens/account/publishers/publisher_new.dart';
import 'package:surface/screens/account/publishers/publishers.dart'; import 'package:surface/screens/account/publishers/publishers.dart';
import 'package:surface/screens/album.dart';
import 'package:surface/screens/auth/login.dart'; import 'package:surface/screens/auth/login.dart';
import 'package:surface/screens/auth/register.dart'; import 'package:surface/screens/auth/register.dart';
import 'package:surface/screens/chat.dart';
import 'package:surface/screens/explore.dart'; import 'package:surface/screens/explore.dart';
import 'package:surface/screens/home.dart'; import 'package:surface/screens/home.dart';
import 'package:surface/screens/post/post_detail.dart'; import 'package:surface/screens/post/post_detail.dart';
@@ -20,6 +22,7 @@ final appRouter = GoRouter(
builder: (context, state, child) => AppScaffold( builder: (context, state, child) => AppScaffold(
body: child, body: child,
showBottomNavigation: true, showBottomNavigation: true,
showDrawer: true,
), ),
routes: [ routes: [
GoRoute( GoRoute(
@@ -37,6 +40,16 @@ final appRouter = GoRouter(
name: 'account', name: 'account',
builder: (context, state) => const AccountScreen(), builder: (context, state) => const AccountScreen(),
), ),
GoRoute(
path: '/chat',
name: 'chat',
builder: (context, state) => const ChatScreen(),
),
GoRoute(
path: '/album',
name: 'album',
builder: (context, state) => const AlbumScreen(),
),
], ],
), ),
ShellRoute( ShellRoute(
@@ -74,6 +87,7 @@ final appRouter = GoRouter(
builder: (context, state, child) => AppScaffold( builder: (context, state, child) => AppScaffold(
body: child, body: child,
autoImplyAppBar: true, autoImplyAppBar: true,
showDrawer: true,
), ),
routes: [ routes: [
GoRoute( GoRoute(

10
lib/screens/album.dart Normal file
View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class AlbumScreen extends StatelessWidget {
const AlbumScreen({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@@ -271,13 +271,14 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
try { try {
// Request one-time-password code // Request one-time-password code
sn.client.post('/cgi/id/auth/factors/$_factorPicked'); await sn.client.post('/cgi/id/auth/factors/$_factorPicked');
widget.onPickFactor( widget.onPickFactor(
widget.factors!.where((x) => x.id == _factorPicked).first, widget.factors!.where((x) => x.id == _factorPicked).first,
); );
widget.onNext(); widget.onNext();
} catch (err) { } catch (err) {
context.showErrorDialog(err); // ignore: use_build_context_synchronously
if (context.mounted) context.showErrorDialog(err);
return; return;
} finally { } finally {
setState(() => _isBusy = false); setState(() => _isBusy = false);

10
lib/screens/chat.dart Normal file
View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class ChatScreen extends StatelessWidget {
const ChatScreen({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@@ -47,13 +47,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
resp.data['body']['attachments']?.cast<String>() ?? [], resp.data['body']['attachments']?.cast<String>() ?? [],
); );
if (!mounted) return; if (!mounted) return;
setState(() {
_data = SnPost.fromJson(resp.data).copyWith( _data = SnPost.fromJson(resp.data).copyWith(
preload: SnPostPreload( preload: SnPostPreload(
attachments: attachments, attachments: attachments,
), ),
); );
});
} catch (err) { } catch (err) {
context.showErrorDialog(err); context.showErrorDialog(err);
} finally { } finally {
@@ -87,13 +85,19 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
}, },
), ),
flexibleSpace: Column( flexibleSpace: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (_data?.body['title'] != null)
Text(_data?.body['title'] ?? 'postNoun'.tr()) Text(_data?.body['title'] ?? 'postNoun'.tr())
.textStyle(Theme.of(context).textTheme.titleLarge!) .textStyle(Theme.of(context).textTheme.titleLarge!)
.textColor(Colors.white), .textColor(Colors.white),
Text('postDetail') if (_data?.body['title'] != null)
.tr() Text('postDetail'.tr())
.textColor(Colors.white.withAlpha((255 * 0.9).round())), .textColor(Colors.white.withAlpha((255 * 0.9).round()))
else
Text('postDetail'.tr())
.textStyle(Theme.of(context).textTheme.titleLarge!)
.textColor(Colors.white),
], ],
).padding(top: math.max(MediaQuery.of(context).padding.top, 8)), ).padding(top: math.max(MediaQuery.of(context).padding.top, 8)),
), ),
@@ -104,7 +108,10 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
), ),
if (_data != null) if (_data != null)
SliverToBoxAdapter( SliverToBoxAdapter(
child: PostItem(data: _data!, showComments: false), child: PostItem(
data: _data!,
showComments: false,
),
), ),
const SliverToBoxAdapter(child: Divider(height: 1)), const SliverToBoxAdapter(child: Divider(height: 1)),
if (_data != null) if (_data != null)

View File

@@ -20,13 +20,13 @@ class SnPost with _$SnPost {
required String? aliasPrefix, required String? aliasPrefix,
required List<dynamic> tags, required List<dynamic> tags,
required List<dynamic> categories, required List<dynamic> categories,
required dynamic replies, required List<SnPost>? replies,
required dynamic replyId, required int? replyId,
required dynamic repostId, required int? repostId,
required dynamic replyTo, required SnPost? replyTo,
required dynamic repostTo, required SnPost? repostTo,
required dynamic visibleUsersList, required List<int>? visibleUsersList,
required dynamic invisibleUsersList, required List<int>? invisibleUsersList,
required int visibility, required int visibility,
required DateTime? editedAt, required DateTime? editedAt,
required DateTime? pinnedAt, required DateTime? pinnedAt,

View File

@@ -31,13 +31,13 @@ mixin _$SnPost {
String? get aliasPrefix => throw _privateConstructorUsedError; String? get aliasPrefix => throw _privateConstructorUsedError;
List<dynamic> get tags => throw _privateConstructorUsedError; List<dynamic> get tags => throw _privateConstructorUsedError;
List<dynamic> get categories => throw _privateConstructorUsedError; List<dynamic> get categories => throw _privateConstructorUsedError;
dynamic get replies => throw _privateConstructorUsedError; List<SnPost>? get replies => throw _privateConstructorUsedError;
dynamic get replyId => throw _privateConstructorUsedError; int? get replyId => throw _privateConstructorUsedError;
dynamic get repostId => throw _privateConstructorUsedError; int? get repostId => throw _privateConstructorUsedError;
dynamic get replyTo => throw _privateConstructorUsedError; SnPost? get replyTo => throw _privateConstructorUsedError;
dynamic get repostTo => throw _privateConstructorUsedError; SnPost? get repostTo => throw _privateConstructorUsedError;
dynamic get visibleUsersList => throw _privateConstructorUsedError; List<int>? get visibleUsersList => throw _privateConstructorUsedError;
dynamic get invisibleUsersList => throw _privateConstructorUsedError; List<int>? get invisibleUsersList => throw _privateConstructorUsedError;
int get visibility => throw _privateConstructorUsedError; int get visibility => throw _privateConstructorUsedError;
DateTime? get editedAt => throw _privateConstructorUsedError; DateTime? get editedAt => throw _privateConstructorUsedError;
DateTime? get pinnedAt => throw _privateConstructorUsedError; DateTime? get pinnedAt => throw _privateConstructorUsedError;
@@ -78,13 +78,13 @@ abstract class $SnPostCopyWith<$Res> {
String? aliasPrefix, String? aliasPrefix,
List<dynamic> tags, List<dynamic> tags,
List<dynamic> categories, List<dynamic> categories,
dynamic replies, List<SnPost>? replies,
dynamic replyId, int? replyId,
dynamic repostId, int? repostId,
dynamic replyTo, SnPost? replyTo,
dynamic repostTo, SnPost? repostTo,
dynamic visibleUsersList, List<int>? visibleUsersList,
dynamic invisibleUsersList, List<int>? invisibleUsersList,
int visibility, int visibility,
DateTime? editedAt, DateTime? editedAt,
DateTime? pinnedAt, DateTime? pinnedAt,
@@ -99,6 +99,8 @@ abstract class $SnPostCopyWith<$Res> {
SnMetric metric, SnMetric metric,
SnPostPreload? preload}); SnPostPreload? preload});
$SnPostCopyWith<$Res>? get replyTo;
$SnPostCopyWith<$Res>? get repostTo;
$SnPublisherCopyWith<$Res> get publisher; $SnPublisherCopyWith<$Res> get publisher;
$SnMetricCopyWith<$Res> get metric; $SnMetricCopyWith<$Res> get metric;
$SnPostPreloadCopyWith<$Res>? get preload; $SnPostPreloadCopyWith<$Res>? get preload;
@@ -199,31 +201,31 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
replies: freezed == replies replies: freezed == replies
? _value.replies ? _value.replies
: replies // ignore: cast_nullable_to_non_nullable : replies // ignore: cast_nullable_to_non_nullable
as dynamic, as List<SnPost>?,
replyId: freezed == replyId replyId: freezed == replyId
? _value.replyId ? _value.replyId
: replyId // ignore: cast_nullable_to_non_nullable : replyId // ignore: cast_nullable_to_non_nullable
as dynamic, as int?,
repostId: freezed == repostId repostId: freezed == repostId
? _value.repostId ? _value.repostId
: repostId // ignore: cast_nullable_to_non_nullable : repostId // ignore: cast_nullable_to_non_nullable
as dynamic, as int?,
replyTo: freezed == replyTo replyTo: freezed == replyTo
? _value.replyTo ? _value.replyTo
: replyTo // ignore: cast_nullable_to_non_nullable : replyTo // ignore: cast_nullable_to_non_nullable
as dynamic, as SnPost?,
repostTo: freezed == repostTo repostTo: freezed == repostTo
? _value.repostTo ? _value.repostTo
: repostTo // ignore: cast_nullable_to_non_nullable : repostTo // ignore: cast_nullable_to_non_nullable
as dynamic, as SnPost?,
visibleUsersList: freezed == visibleUsersList visibleUsersList: freezed == visibleUsersList
? _value.visibleUsersList ? _value.visibleUsersList
: visibleUsersList // ignore: cast_nullable_to_non_nullable : visibleUsersList // ignore: cast_nullable_to_non_nullable
as dynamic, as List<int>?,
invisibleUsersList: freezed == invisibleUsersList invisibleUsersList: freezed == invisibleUsersList
? _value.invisibleUsersList ? _value.invisibleUsersList
: invisibleUsersList // ignore: cast_nullable_to_non_nullable : invisibleUsersList // ignore: cast_nullable_to_non_nullable
as dynamic, as List<int>?,
visibility: null == visibility visibility: null == visibility
? _value.visibility ? _value.visibility
: visibility // ignore: cast_nullable_to_non_nullable : visibility // ignore: cast_nullable_to_non_nullable
@@ -279,6 +281,34 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
) as $Val); ) as $Val);
} }
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostCopyWith<$Res>? get replyTo {
if (_value.replyTo == null) {
return null;
}
return $SnPostCopyWith<$Res>(_value.replyTo!, (value) {
return _then(_value.copyWith(replyTo: value) as $Val);
});
}
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostCopyWith<$Res>? get repostTo {
if (_value.repostTo == null) {
return null;
}
return $SnPostCopyWith<$Res>(_value.repostTo!, (value) {
return _then(_value.copyWith(repostTo: value) as $Val);
});
}
/// Create a copy of SnPost /// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@@ -333,13 +363,13 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
String? aliasPrefix, String? aliasPrefix,
List<dynamic> tags, List<dynamic> tags,
List<dynamic> categories, List<dynamic> categories,
dynamic replies, List<SnPost>? replies,
dynamic replyId, int? replyId,
dynamic repostId, int? repostId,
dynamic replyTo, SnPost? replyTo,
dynamic repostTo, SnPost? repostTo,
dynamic visibleUsersList, List<int>? visibleUsersList,
dynamic invisibleUsersList, List<int>? invisibleUsersList,
int visibility, int visibility,
DateTime? editedAt, DateTime? editedAt,
DateTime? pinnedAt, DateTime? pinnedAt,
@@ -354,6 +384,10 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
SnMetric metric, SnMetric metric,
SnPostPreload? preload}); SnPostPreload? preload});
@override
$SnPostCopyWith<$Res>? get replyTo;
@override
$SnPostCopyWith<$Res>? get repostTo;
@override @override
$SnPublisherCopyWith<$Res> get publisher; $SnPublisherCopyWith<$Res> get publisher;
@override @override
@@ -453,33 +487,33 @@ class __$$SnPostImplCopyWithImpl<$Res>
: categories // ignore: cast_nullable_to_non_nullable : categories // ignore: cast_nullable_to_non_nullable
as List<dynamic>, as List<dynamic>,
replies: freezed == replies replies: freezed == replies
? _value.replies ? _value._replies
: replies // ignore: cast_nullable_to_non_nullable : replies // ignore: cast_nullable_to_non_nullable
as dynamic, as List<SnPost>?,
replyId: freezed == replyId replyId: freezed == replyId
? _value.replyId ? _value.replyId
: replyId // ignore: cast_nullable_to_non_nullable : replyId // ignore: cast_nullable_to_non_nullable
as dynamic, as int?,
repostId: freezed == repostId repostId: freezed == repostId
? _value.repostId ? _value.repostId
: repostId // ignore: cast_nullable_to_non_nullable : repostId // ignore: cast_nullable_to_non_nullable
as dynamic, as int?,
replyTo: freezed == replyTo replyTo: freezed == replyTo
? _value.replyTo ? _value.replyTo
: replyTo // ignore: cast_nullable_to_non_nullable : replyTo // ignore: cast_nullable_to_non_nullable
as dynamic, as SnPost?,
repostTo: freezed == repostTo repostTo: freezed == repostTo
? _value.repostTo ? _value.repostTo
: repostTo // ignore: cast_nullable_to_non_nullable : repostTo // ignore: cast_nullable_to_non_nullable
as dynamic, as SnPost?,
visibleUsersList: freezed == visibleUsersList visibleUsersList: freezed == visibleUsersList
? _value.visibleUsersList ? _value._visibleUsersList
: visibleUsersList // ignore: cast_nullable_to_non_nullable : visibleUsersList // ignore: cast_nullable_to_non_nullable
as dynamic, as List<int>?,
invisibleUsersList: freezed == invisibleUsersList invisibleUsersList: freezed == invisibleUsersList
? _value.invisibleUsersList ? _value._invisibleUsersList
: invisibleUsersList // ignore: cast_nullable_to_non_nullable : invisibleUsersList // ignore: cast_nullable_to_non_nullable
as dynamic, as List<int>?,
visibility: null == visibility visibility: null == visibility
? _value.visibility ? _value.visibility
: visibility // ignore: cast_nullable_to_non_nullable : visibility // ignore: cast_nullable_to_non_nullable
@@ -551,13 +585,13 @@ class _$SnPostImpl extends _SnPost {
required this.aliasPrefix, required this.aliasPrefix,
required final List<dynamic> tags, required final List<dynamic> tags,
required final List<dynamic> categories, required final List<dynamic> categories,
required this.replies, required final List<SnPost>? replies,
required this.replyId, required this.replyId,
required this.repostId, required this.repostId,
required this.replyTo, required this.replyTo,
required this.repostTo, required this.repostTo,
required this.visibleUsersList, required final List<int>? visibleUsersList,
required this.invisibleUsersList, required final List<int>? invisibleUsersList,
required this.visibility, required this.visibility,
required this.editedAt, required this.editedAt,
required this.pinnedAt, required this.pinnedAt,
@@ -574,6 +608,9 @@ class _$SnPostImpl extends _SnPost {
: _body = body, : _body = body,
_tags = tags, _tags = tags,
_categories = categories, _categories = categories,
_replies = replies,
_visibleUsersList = visibleUsersList,
_invisibleUsersList = invisibleUsersList,
super._(); super._();
factory _$SnPostImpl.fromJson(Map<String, dynamic> json) => factory _$SnPostImpl.fromJson(Map<String, dynamic> json) =>
@@ -619,20 +656,46 @@ class _$SnPostImpl extends _SnPost {
return EqualUnmodifiableListView(_categories); return EqualUnmodifiableListView(_categories);
} }
final List<SnPost>? _replies;
@override @override
final dynamic replies; List<SnPost>? get replies {
final value = _replies;
if (value == null) return null;
if (_replies is EqualUnmodifiableListView) return _replies;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override @override
final dynamic replyId; final int? replyId;
@override @override
final dynamic repostId; final int? repostId;
@override @override
final dynamic replyTo; final SnPost? replyTo;
@override @override
final dynamic repostTo; final SnPost? repostTo;
final List<int>? _visibleUsersList;
@override @override
final dynamic visibleUsersList; List<int>? get visibleUsersList {
final value = _visibleUsersList;
if (value == null) return null;
if (_visibleUsersList is EqualUnmodifiableListView)
return _visibleUsersList;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
final List<int>? _invisibleUsersList;
@override @override
final dynamic invisibleUsersList; List<int>? get invisibleUsersList {
final value = _invisibleUsersList;
if (value == null) return null;
if (_invisibleUsersList is EqualUnmodifiableListView)
return _invisibleUsersList;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override @override
final int visibility; final int visibility;
@override @override
@@ -687,15 +750,17 @@ class _$SnPostImpl extends _SnPost {
const DeepCollectionEquality().equals(other._tags, _tags) && const DeepCollectionEquality().equals(other._tags, _tags) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other._categories, _categories) && .equals(other._categories, _categories) &&
const DeepCollectionEquality().equals(other.replies, replies) && const DeepCollectionEquality().equals(other._replies, _replies) &&
const DeepCollectionEquality().equals(other.replyId, replyId) && (identical(other.replyId, replyId) || other.replyId == replyId) &&
const DeepCollectionEquality().equals(other.repostId, repostId) && (identical(other.repostId, repostId) ||
const DeepCollectionEquality().equals(other.replyTo, replyTo) && other.repostId == repostId) &&
const DeepCollectionEquality().equals(other.repostTo, repostTo) && (identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
(identical(other.repostTo, repostTo) ||
other.repostTo == repostTo) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other.visibleUsersList, visibleUsersList) && .equals(other._visibleUsersList, _visibleUsersList) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other.invisibleUsersList, invisibleUsersList) && .equals(other._invisibleUsersList, _invisibleUsersList) &&
(identical(other.visibility, visibility) || (identical(other.visibility, visibility) ||
other.visibility == visibility) && other.visibility == visibility) &&
(identical(other.editedAt, editedAt) || (identical(other.editedAt, editedAt) ||
@@ -736,13 +801,13 @@ class _$SnPostImpl extends _SnPost {
aliasPrefix, aliasPrefix,
const DeepCollectionEquality().hash(_tags), const DeepCollectionEquality().hash(_tags),
const DeepCollectionEquality().hash(_categories), const DeepCollectionEquality().hash(_categories),
const DeepCollectionEquality().hash(replies), const DeepCollectionEquality().hash(_replies),
const DeepCollectionEquality().hash(replyId), replyId,
const DeepCollectionEquality().hash(repostId), repostId,
const DeepCollectionEquality().hash(replyTo), replyTo,
const DeepCollectionEquality().hash(repostTo), repostTo,
const DeepCollectionEquality().hash(visibleUsersList), const DeepCollectionEquality().hash(_visibleUsersList),
const DeepCollectionEquality().hash(invisibleUsersList), const DeepCollectionEquality().hash(_invisibleUsersList),
visibility, visibility,
editedAt, editedAt,
pinnedAt, pinnedAt,
@@ -787,13 +852,13 @@ abstract class _SnPost extends SnPost {
required final String? aliasPrefix, required final String? aliasPrefix,
required final List<dynamic> tags, required final List<dynamic> tags,
required final List<dynamic> categories, required final List<dynamic> categories,
required final dynamic replies, required final List<SnPost>? replies,
required final dynamic replyId, required final int? replyId,
required final dynamic repostId, required final int? repostId,
required final dynamic replyTo, required final SnPost? replyTo,
required final dynamic repostTo, required final SnPost? repostTo,
required final dynamic visibleUsersList, required final List<int>? visibleUsersList,
required final dynamic invisibleUsersList, required final List<int>? invisibleUsersList,
required final int visibility, required final int visibility,
required final DateTime? editedAt, required final DateTime? editedAt,
required final DateTime? pinnedAt, required final DateTime? pinnedAt,
@@ -834,19 +899,19 @@ abstract class _SnPost extends SnPost {
@override @override
List<dynamic> get categories; List<dynamic> get categories;
@override @override
dynamic get replies; List<SnPost>? get replies;
@override @override
dynamic get replyId; int? get replyId;
@override @override
dynamic get repostId; int? get repostId;
@override @override
dynamic get replyTo; SnPost? get replyTo;
@override @override
dynamic get repostTo; SnPost? get repostTo;
@override @override
dynamic get visibleUsersList; List<int>? get visibleUsersList;
@override @override
dynamic get invisibleUsersList; List<int>? get invisibleUsersList;
@override @override
int get visibility; int get visibility;
@override @override

View File

@@ -20,13 +20,23 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
aliasPrefix: json['alias_prefix'] as String?, aliasPrefix: json['alias_prefix'] as String?,
tags: json['tags'] as List<dynamic>, tags: json['tags'] as List<dynamic>,
categories: json['categories'] as List<dynamic>, categories: json['categories'] as List<dynamic>,
replies: json['replies'], replies: (json['replies'] as List<dynamic>?)
replyId: json['reply_id'], ?.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
repostId: json['repost_id'], .toList(),
replyTo: json['reply_to'], replyId: (json['reply_id'] as num?)?.toInt(),
repostTo: json['repost_to'], repostId: (json['repost_id'] as num?)?.toInt(),
visibleUsersList: json['visible_users_list'], replyTo: json['reply_to'] == null
invisibleUsersList: json['invisible_users_list'], ? null
: SnPost.fromJson(json['reply_to'] as Map<String, dynamic>),
repostTo: json['repost_to'] == null
? null
: SnPost.fromJson(json['repost_to'] as Map<String, dynamic>),
visibleUsersList: (json['visible_users_list'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
invisibleUsersList: (json['invisible_users_list'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
visibility: (json['visibility'] as num).toInt(), visibility: (json['visibility'] as num).toInt(),
editedAt: json['edited_at'] == null editedAt: json['edited_at'] == null
? null ? null
@@ -68,11 +78,11 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
'alias_prefix': instance.aliasPrefix, 'alias_prefix': instance.aliasPrefix,
'tags': instance.tags, 'tags': instance.tags,
'categories': instance.categories, 'categories': instance.categories,
'replies': instance.replies, 'replies': instance.replies?.map((e) => e.toJson()).toList(),
'reply_id': instance.replyId, 'reply_id': instance.replyId,
'repost_id': instance.repostId, 'repost_id': instance.repostId,
'reply_to': instance.replyTo, 'reply_to': instance.replyTo?.toJson(),
'repost_to': instance.repostTo, 'repost_to': instance.repostTo?.toJson(),
'visible_users_list': instance.visibleUsersList, 'visible_users_list': instance.visibleUsersList,
'invisible_users_list': instance.invisibleUsersList, 'invisible_users_list': instance.invisibleUsersList,
'visibility': instance.visibility, 'visibility': instance.visibility,

View File

@@ -28,6 +28,9 @@ class AttachmentDetailPopup extends StatelessWidget {
tag: 'attachment-${data.rid}-${heroTag ?? uuid.v4()}', tag: 'attachment-${data.rid}-${heroTag ?? uuid.v4()}',
child: PhotoView( child: PhotoView(
key: Key('attachment-detail-${data.rid}-$heroTag'), key: Key('attachment-detail-${data.rid}-$heroTag'),
backgroundDecoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
),
imageProvider: UniversalImage.provider( imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(data.rid), sn.getAttachmentUrl(data.rid),
), ),

View File

@@ -1,6 +1,12 @@
import 'dart:ui';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_detail.dart'; import 'package:surface/widgets/attachment/attachment_detail.dart';
@@ -23,15 +29,11 @@ class AttachmentItem extends StatelessWidget {
case 'image': case 'image':
return Hero( return Hero(
tag: 'attachment-${data.rid}-$heroTag', tag: 'attachment-${data.rid}-$heroTag',
child: LayoutBuilder(builder: (context, constraints) { child: AutoResizeUniversalImage(
return UniversalImage(
sn.getAttachmentUrl(data.rid), sn.getAttachmentUrl(data.rid),
key: Key('attachment-${data.rid}-$heroTag'), key: Key('attachment-${data.rid}-$heroTag'),
fit: BoxFit.cover, fit: BoxFit.cover,
cacheHeight: constraints.maxHeight, ),
cacheWidth: constraints.maxWidth,
);
}),
); );
default: default:
return const Placeholder(); return const Placeholder();
@@ -43,6 +45,12 @@ class AttachmentItem extends StatelessWidget {
final uuid = Uuid(); final uuid = Uuid();
final heroTag = uuid.v4(); final heroTag = uuid.v4();
if (data.isMature) {
return _AttachmentItemSensitiveBlur(
child: _buildContent(context, heroTag),
);
}
if (isExpandable) { if (isExpandable) {
return GestureDetector( return GestureDetector(
child: _buildContent(context, heroTag), child: _buildContent(context, heroTag),
@@ -58,3 +66,87 @@ class AttachmentItem extends StatelessWidget {
return _buildContent(context, heroTag); return _buildContent(context, heroTag);
} }
} }
class _AttachmentItemSensitiveBlur extends StatefulWidget {
final Widget child;
const _AttachmentItemSensitiveBlur({super.key, required this.child});
@override
State<_AttachmentItemSensitiveBlur> createState() =>
_AttachmentItemSensitiveBlurState();
}
class _AttachmentItemSensitiveBlurState
extends State<_AttachmentItemSensitiveBlur> {
bool _doesShow = false;
@override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40),
child: Container(
color: Colors.black.withOpacity(0.5),
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Symbols.visibility_off,
color: Colors.white,
size: 32,
),
const Gap(8),
Text('sensitiveContent')
.tr()
.fontSize(20)
.textColor(Colors.white)
.bold(),
Text('sensitiveContentDescription')
.tr()
.fontSize(14)
.textColor(Colors.white.withOpacity(0.8)),
const Gap(16),
InkWell(
child: Text('sensitiveContentReveal')
.tr()
.textColor(Colors.white),
onTap: () {
setState(() => _doesShow = !_doesShow);
},
),
],
),
),
),
)
.opacity(_doesShow ? 0 : 1, animate: true)
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
if (_doesShow)
Positioned(
top: 0,
left: 0,
child: InkWell(
child: Icon(
Symbols.visibility_off,
color: Colors.white,
shadows: [
BoxShadow(
color: Colors.black.withOpacity(0.5),
blurRadius: 3,
offset: Offset(0, 1.5),
),
],
).padding(all: 12),
onTap: () {
setState(() => _doesShow = !_doesShow);
},
),
),
],
);
}
}

View File

@@ -10,15 +10,15 @@ import 'package:surface/widgets/attachment/attachment_item.dart';
class AttachmentList extends StatelessWidget { class AttachmentList extends StatelessWidget {
final List<SnAttachment> data; final List<SnAttachment> data;
final bool? bordered; final bool? bordered;
final double? maxListHeight; final double? maxHeight;
const AttachmentList({ const AttachmentList({
super.key, super.key,
required this.data, required this.data,
this.bordered, this.bordered,
this.maxListHeight, this.maxHeight,
}); });
static const double kMaxListItemWidth = 520; static const double kMaxItemWidth = 520;
static const BorderRadius kDefaultRadius = static const BorderRadius kDefaultRadius =
BorderRadius.all(Radius.circular(8)); BorderRadius.all(Radius.circular(8));
@@ -33,9 +33,10 @@ class AttachmentList extends StatelessWidget {
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) { if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) {
return Container( return Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: maxHeight ?? double.infinity,
maxWidth: math.min( maxWidth: math.min(
MediaQuery.of(context).size.width - 20, MediaQuery.of(context).size.width - 20,
kMaxListItemWidth, kMaxItemWidth,
), ),
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -64,7 +65,7 @@ class AttachmentList extends StatelessWidget {
} }
return Container( return Container(
constraints: BoxConstraints(maxHeight: maxListHeight ?? 320), constraints: BoxConstraints(maxHeight: maxHeight ?? 320),
child: ScrollConfiguration( child: ScrollConfiguration(
behavior: _AttachmentListScrollBehavior(), behavior: _AttachmentListScrollBehavior(),
child: ListView.separated( child: ListView.separated(
@@ -73,9 +74,10 @@ class AttachmentList extends StatelessWidget {
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
return Container( return Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: maxHeight ?? double.infinity,
maxWidth: math.min( maxWidth: math.min(
MediaQuery.of(context).size.width - 20, MediaQuery.of(context).size.width - 20,
kMaxListItemWidth, kMaxItemWidth,
), ),
), ),
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@@ -1,6 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:surface/widgets/navigation/app_destinations.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/navigation.dart';
class AppBottomNavigationBar extends StatefulWidget { class AppBottomNavigationBar extends StatefulWidget {
const AppBottomNavigationBar({super.key}); const AppBottomNavigationBar({super.key});
@@ -10,23 +12,46 @@ class AppBottomNavigationBar extends StatefulWidget {
} }
class _AppBottomNavigationBarState extends State<AppBottomNavigationBar> { class _AppBottomNavigationBarState extends State<AppBottomNavigationBar> {
int _currentIndex = 0; @override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context
.read<NavigationProvider>()
.autoDetectIndex(GoRouter.maybeOf(context));
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final nav = context.watch<NavigationProvider>();
return ListenableBuilder(
listenable: nav,
builder: (context, _) {
if (!nav.isIndexInRange(0, nav.pinnedDestinationCount)) {
return const SizedBox.shrink();
}
final destinations = [
...nav.destinations.where((ele) => ele.isPinned),
];
return BottomNavigationBar( return BottomNavigationBar(
currentIndex: _currentIndex, currentIndex: nav.getIndexInRange(0, nav.pinnedDestinationCount),
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
showUnselectedLabels: false, showUnselectedLabels: false,
items: appDestinations.map((ele) { items: destinations.map((ele) {
return BottomNavigationBarItem( return BottomNavigationBarItem(
icon: ele.icon, icon: ele.icon,
label: ele.label, label: ele.label.tr(),
); );
}).toList(), }).toList(),
onTap: (idx) { onTap: (idx) {
setState(() => _currentIndex = idx); nav.setIndex(idx);
GoRouter.of(context).goNamed(appDestinations[idx].screen); GoRouter.of(context).goNamed(destinations[idx].screen);
},
);
}, },
); );
} }

View File

@@ -1,33 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
class AppNavDestination {
final String label;
final String screen;
final Widget icon;
AppNavDestination({
required this.label,
required this.screen,
required this.icon,
});
}
List<AppNavDestination> appDestinations = [
AppNavDestination(
icon: Icon(Symbols.home, weight: 400, opticalSize: 20),
screen: 'home',
label: tr('screenHome'),
),
AppNavDestination(
icon: Icon(Symbols.explore, weight: 400, opticalSize: 20),
screen: 'explore',
label: tr('screenExplore'),
),
AppNavDestination(
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
screen: 'account',
label: tr('screenAccount'),
),
];

View File

@@ -0,0 +1,85 @@
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/navigation.dart';
class AppNavigationDrawer extends StatefulWidget {
const AppNavigationDrawer({super.key});
@override
State<AppNavigationDrawer> createState() => _AppNavigationDrawerState();
}
class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context
.read<NavigationProvider>()
.autoDetectIndex(GoRouter.maybeOf(context));
});
}
@override
Widget build(BuildContext context) {
final nav = context.watch<NavigationProvider>();
final backgroundColor = ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? Theme.of(context).colorScheme.surface
: null;
return ListenableBuilder(
listenable: nav,
builder: (context, _) {
final destinations = [
...nav.destinations.where((ele) => ele.isPinned),
...nav.destinations.where((ele) => !ele.isPinned),
];
return NavigationDrawer(
backgroundColor: backgroundColor,
selectedIndex: nav.currentIndex,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Solar Network').bold(),
Text('Solar Network 2.0α').fontSize(12).textColor(
Theme.of(context).colorScheme.onSurface.withOpacity(0.5)),
],
).padding(
horizontal: 32,
top: math.max(MediaQuery.of(context).padding.top, 16),
bottom: 16,
),
...destinations.where((ele) => ele.isPinned).map((ele) {
return NavigationDrawerDestination(
icon: ele.icon,
label: Text(ele.label).tr(),
);
}),
const Divider(),
...destinations.where((ele) => !ele.isPinned).map((ele) {
return NavigationDrawerDestination(
icon: ele.icon,
label: Text(ele.label).tr(),
);
}),
],
onDestinationSelected: (idx) {
nav.setIndex(idx);
GoRouter.of(context).goNamed(destinations[idx].screen);
Scaffold.of(context).closeDrawer();
},
);
},
);
}
}

View File

@@ -5,6 +5,7 @@ import 'package:responsive_framework/responsive_framework.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_background.dart'; import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_bottom_navigation.dart'; import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
class AppScaffold extends StatelessWidget { class AppScaffold extends StatelessWidget {
final PreferredSizeWidget? appBar; final PreferredSizeWidget? appBar;
@@ -14,6 +15,7 @@ class AppScaffold extends StatelessWidget {
final Widget? body; final Widget? body;
final bool autoImplyAppBar; final bool autoImplyAppBar;
final bool showBottomNavigation; final bool showBottomNavigation;
final bool showDrawer;
const AppScaffold({ const AppScaffold({
super.key, super.key,
this.appBar, this.appBar,
@@ -23,17 +25,21 @@ class AppScaffold extends StatelessWidget {
this.body, this.body,
this.autoImplyAppBar = false, this.autoImplyAppBar = false,
this.showBottomNavigation = false, this.showBottomNavigation = false,
this.showDrawer = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isShowDrawer = showDrawer
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
: false;
final isShowBottomNavigation = (showBottomNavigation) final isShowBottomNavigation = (showBottomNavigation)
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE) ? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
: false; : false;
final state = GoRouter.maybeOf(context); final state = GoRouter.maybeOf(context);
return AppBackground( final innerWidget = AppBackground(
child: Scaffold( child: Scaffold(
appBar: appBar ?? appBar: appBar ??
(autoImplyAppBar (autoImplyAppBar
@@ -50,9 +56,22 @@ class AppScaffold extends StatelessWidget {
body: body, body: body,
floatingActionButtonLocation: floatingActionButtonLocation, floatingActionButtonLocation: floatingActionButtonLocation,
floatingActionButton: floatingActionButton, floatingActionButton: floatingActionButton,
drawer: isShowDrawer ? AppNavigationDrawer() : null,
bottomNavigationBar: bottomNavigationBar:
isShowBottomNavigation ? AppBottomNavigationBar() : null, isShowBottomNavigation ? AppBottomNavigationBar() : null,
), ),
); );
if (showDrawer) {
return Row(
children: [
AppNavigationDrawer(),
VerticalDivider(width: 1, color: Theme.of(context).dividerColor),
Expanded(child: innerWidget),
],
);
}
return innerWidget;
} }
} }

View File

@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
@@ -33,16 +34,25 @@ class PostItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isListAttachments =
ResponsiveBreakpoints.of(context).largerThan(MOBILE) ||
(data.preload?.attachments?.length ?? 0) > 1;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_PostContentHeader(data: data).padding(horizontal: 12, vertical: 8), _PostContentHeader(data: data).padding(horizontal: 12, vertical: 8),
_PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6), _PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6),
if (data.repostTo != null)
_PostQuoteContent(child: data.repostTo!).padding(
horizontal: 12,
),
if (data.preload?.attachments?.isNotEmpty ?? true) if (data.preload?.attachments?.isNotEmpty ?? true)
AttachmentList( AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
bordered: true, bordered: true,
), maxHeight: 520,
).padding(horizontal: isListAttachments ? 12 : 0),
_PostBottomAction( _PostBottomAction(
data: data, data: data,
showComments: showComments, showComments: showComments,
@@ -148,7 +158,13 @@ class _PostBottomAction extends StatelessWidget {
class _PostContentHeader extends StatelessWidget { class _PostContentHeader extends StatelessWidget {
final SnPost data; final SnPost data;
const _PostContentHeader({required this.data}); final bool isCompact;
final bool showActions;
const _PostContentHeader({
required this.data,
this.isCompact = false,
this.showActions = true,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -157,8 +173,28 @@ class _PostContentHeader extends StatelessWidget {
return Row( return Row(
children: [ children: [
AccountImage(content: data.publisher.avatar), AccountImage(
const Gap(12), content: data.publisher.avatar,
radius: isCompact ? 12 : 20,
),
Gap(isCompact ? 8 : 12),
if (isCompact)
Row(
children: [
Text(data.publisher.nick).bold(),
const Gap(4),
Row(
children: [
Text('@${data.publisher.name}').fontSize(13),
const Gap(4),
Text(RelativeTime(context).format(
data.publishedAt ?? data.createdAt,
)).fontSize(13),
],
).opacity(0.8),
],
)
else
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -176,6 +212,7 @@ class _PostContentHeader extends StatelessWidget {
], ],
), ),
), ),
if (showActions)
PopupMenuButton( PopupMenuButton(
icon: const Icon(Symbols.more_horiz), icon: const Icon(Symbols.more_horiz),
style: const ButtonStyle( style: const ButtonStyle(
@@ -269,3 +306,29 @@ class _PostContentBody extends StatelessWidget {
return MarkdownTextContent(content: data['content']); return MarkdownTextContent(content: data['content']);
} }
} }
class _PostQuoteContent extends StatelessWidget {
final SnPost child;
const _PostQuoteContent({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
children: [
_PostContentHeader(data: child, isCompact: true, showActions: false)
.padding(bottom: 4),
_PostContentBody(data: child.body),
],
),
);
}
}

View File

@@ -5,23 +5,23 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "76.0.0" version: "72.0.0"
_macros: _macros:
dependency: transitive dependency: transitive
description: dart description: dart
source: sdk source: sdk
version: "0.3.3" version: "0.3.2"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.11.0" version: "6.7.0"
animations: animations:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -202,10 +202,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: collection name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.0" version: "1.18.0"
connectivity_plus: connectivity_plus:
dependency: transitive dependency: transitive
description: description:
@@ -790,18 +790,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.8" version: "10.0.5"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.9" version: "3.0.5"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
@@ -838,10 +838,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: macros name: macros
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.3-main.0" version: "0.1.2-main.4"
markdown: markdown:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1150,7 +1150,7 @@ packages:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.99"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:
@@ -1227,10 +1227,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.12.0" version: "1.11.1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
@@ -1251,10 +1251,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: string_scanner name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.2.0"
styled_widget: styled_widget:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1291,10 +1291,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.3" version: "0.7.2"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@@ -1411,10 +1411,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.3.0" version: "14.2.5"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1 version: 2.0.0+2
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4

9
roadsign.toml Normal file
View File

@@ -0,0 +1,9 @@
id = "solian-next"
[[locations]]
id = "solian-next"
host = ["sn-next.solsynth.dev"]
path = ["/"]
[[locations.destinations]]
id = "solian-next-web"
uri = "files:///workdir/solian-next?fallback=index.html&index=index.html"