🐛 Dozen of hot fixes

This commit is contained in:
LittleSheep 2025-06-28 02:40:50 +08:00
parent 9e8f6d57df
commit ff475d43dd
11 changed files with 219 additions and 105 deletions

View File

@ -3,7 +3,7 @@ name: Build Release
on: on:
push: push:
tags: tags:
- '*' - "*"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@ -59,6 +59,7 @@ jobs:
sudo apt-get install -y libnotify-dev sudo apt-get install -y libnotify-dev
sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
sudo apt-get install -y gstreamer-1.0 sudo apt-get install -y gstreamer-1.0
sudo apt-get install -y libsecret-1
- run: flutter pub get - run: flutter pub get
- run: flutter build linux - run: flutter build linux
- name: Archive production artifacts - name: Archive production artifacts
@ -80,4 +81,4 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: build-output-linux-appimage name: build-output-linux-appimage
path: './*.AppImage*' path: "./*.AppImage*"

View File

@ -98,7 +98,7 @@
<receiver <receiver
android:name=".receiver.ReplyReceiver" android:name=".receiver.ReplyReceiver"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="true" />
<service <service
android:name=".service.MessagingService" android:name=".service.MessagingService"

View File

@ -1,14 +1,39 @@
package dev.solsynth.solian package dev.solsynth.solian
import android.content.Intent
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin import io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin
class MainActivity : FlutterActivity() class MainActivity : FlutterActivity()
{ {
private val CHANNEL = "dev.solsynth.solian/notifications"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
// https://github.com/flutter/flutter/issues/153075#issuecomment-2693189362 // https://github.com/flutter/flutter/issues/153075#issuecomment-2693189362
flutterEngine.plugins.add(LegacySharedPreferencesPlugin()) flutterEngine.plugins.add(LegacySharedPreferencesPlugin())
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "initialLink") {
val roomId = intent.getStringExtra("room_id")
if (roomId != null) {
result.success("/rooms/$roomId")
} else {
result.success(null)
}
} else {
result.notImplemented()
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val roomId = intent.getStringExtra("room_id")
if (roomId != null) {
MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger, CHANNEL).invokeMethod("newLink", "/rooms/$roomId")
}
} }
} }

View File

@ -1,48 +1,47 @@
package dev.solsynth.solian.network package dev.solsynth.solian.network
import android.content.Context import android.content.Context
import okhttp3.* import android.content.SharedPreferences
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
class ApiClient(private val context: Context) { class ApiClient(private val context: Context) {
private val client = OkHttpClient() private val client = OkHttpClient()
private val sharedPreferences: SharedPreferences = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
fun sendMessage(roomId: String, content: String, repliedMessageId: String, callback: () -> Unit) { fun sendMessage(roomId: String, message: String, replyTo: String, callback: (Boolean) -> Unit) {
val prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) val token = sharedPreferences.getString("flutter.token", null)
val token = prefs.getString("flutter.token", null) if (token == null) {
val serverUrl = prefs.getString("flutter.serverUrl", null) callback(false)
if (token == null || serverUrl == null) {
return return
} }
val url = "$serverUrl/chat/$roomId/messages" val json = JSONObject().apply {
put("content", message)
val json = JSONObject() put("reply_to", replyTo)
json.put("content", content) }
json.put("replied_message_id", repliedMessageId) val body = json.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
val requestBody = json.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
val request = Request.Builder() val request = Request.Builder()
.url(url) .url("https://solian.dev/api/rooms/$roomId/messages")
.post(requestBody) .header("Authorization", "Bearer $token")
.addHeader("Authorization", "AtField $token") .post(body)
.build() .build()
client.newCall(request).enqueue(object : Callback { client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
// Handle failure callback(false)
callback()
} }
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
// Handle success callback(response.isSuccessful)
callback()
} }
}) })
} }
} }

View File

@ -72,6 +72,7 @@ class MessagingService: FirebaseMessagingService() {
val intent = Intent(this, MainActivity::class.java) val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.putExtra("room_id", roomId)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, pendingIntentFlags) val pendingIntent = PendingIntent.getActivity(this, 0, intent, pendingIntentFlags)
val notificationBuilder = NotificationCompat.Builder(this, "messages") val notificationBuilder = NotificationCompat.Builder(this, "messages")

View File

@ -89,14 +89,32 @@
"authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.", "authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
"authFactorPin": "Pin Code", "authFactorPin": "Pin Code",
"authFactorPinDescription": "It consists of 6 digits. It cannot be used to log in. When performing some dangerous operations, the system will ask you to enter this PIN for confirmation.", "authFactorPinDescription": "It consists of 6 digits. It cannot be used to log in. When performing some dangerous operations, the system will ask you to enter this PIN for confirmation.",
"realms": "Realms",
"createRealm": "Create a Realm",
"createRealmHint": "Meet friends with same interests, build communities, and more.",
"editRealm": "Edit Realm",
"deleteRealm": "Delete Realm",
"deleteRealmHint": "Are you sure to delete this realm? This will also deleted all the channels, publishers, and posts under this realm.",
"explore": "Explore", "explore": "Explore",
"exploreFilterSubscriptions": "Subscriptions", "exploreFilterSubscriptions": "Subscriptions",
"exploreFilterFriends": "Friends", "exploreFilterFriends": "Friends",
"discover": "Discover", "discover": "Discover",
"joinRealm": "Join Realm",
"account": "Account", "account": "Account",
"name": "Name", "name": "Name",
"slug": "Slug", "slug": "Slug",
"slugHint": "The slug will be used in the URL to access this resource, it should be unique and URL safe.", "slugHint": "The slug will be used in the URL to access this resource, it should be unique and URL safe.",
"createChatRoom": "Create a Room",
"editChatRoom": "Edit Room",
"deleteChatRoom": "Delete Room",
"deleteChatRoomHint": "Are you sure to delete this room? This action cannot be undone.",
"chat": "Chat",
"chatTabAll": "All",
"chatTabDirect": "Direct Messages",
"chatTabGroup": "Group Chats",
"chatMessageHint": "Message in {}",
"chatDirectMessageHint": "Message to {}",
"directMessage": "Direct Message",
"loading": "Loading...", "loading": "Loading...",
"descriptionNone": "No description yet.", "descriptionNone": "No description yet.",
"invites": "Invites", "invites": "Invites",
@ -231,6 +249,7 @@
"uploadingProgress": "Uploading {} of {}", "uploadingProgress": "Uploading {} of {}",
"uploadAll": "Upload All", "uploadAll": "Upload All",
"stickerCopyPlaceholder": "Copy Placeholder", "stickerCopyPlaceholder": "Copy Placeholder",
"realmSelection": "Select a Realm",
"individual": "Individual", "individual": "Individual",
"firstPostBadgeName": "First Post", "firstPostBadgeName": "First Post",
"firstPostBadgeDescription": "Created your first post on Solar Network", "firstPostBadgeDescription": "Created your first post on Solar Network",
@ -286,6 +305,10 @@
"levelingProgressExperience": "{} EXP", "levelingProgressExperience": "{} EXP",
"levelingProgressLevel": "Level {}", "levelingProgressLevel": "Level {}",
"fileUploadingProgress": "Uploading file #{}: {}%", "fileUploadingProgress": "Uploading file #{}: {}%",
"removeChatMember": "Remove Chat Room Member",
"removeChatMemberHint": "Are you sure to remove this member from the room?",
"removeRealmMember": "Remove Realm Member",
"removeRealmMemberHint": "Are you sure to remove this member from the realm?",
"memberRole": "Member Role", "memberRole": "Member Role",
"memberRoleHint": "Greater number has higher permission.", "memberRoleHint": "Greater number has higher permission.",
"memberRoleEdit": "Edit role for @{}", "memberRoleEdit": "Edit role for @{}",
@ -293,6 +316,10 @@
"openLinkConfirmDescription": "You're going to leave the Solar Network and open the link ({}) in your browser. It is not related to Solar Network. Beware of phishing and scams.", "openLinkConfirmDescription": "You're going to leave the Solar Network and open the link ({}) in your browser. It is not related to Solar Network. Beware of phishing and scams.",
"brokenLink": "Unable open link {}... It might be broken or missing uri parts...", "brokenLink": "Unable open link {}... It might be broken or missing uri parts...",
"copyToClipboard": "Copy to clipboard", "copyToClipboard": "Copy to clipboard",
"leaveChatRoom": "Leave Chat Room",
"leaveChatRoomHint": "Are you sure to leave this chat room?",
"leaveRealm": "Leave Realm",
"leaveRealmHint": "Are you sure to leave this realm?",
"walletNotFound": "Wallet not found", "walletNotFound": "Wallet not found",
"walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.", "walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.",
"walletCreate": "Create a Wallet", "walletCreate": "Create a Wallet",
@ -304,6 +331,12 @@
"settingsBackgroundImageClear": "Clear Background Image", "settingsBackgroundImageClear": "Clear Background Image",
"settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image", "settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
"messageNone": "No content to display", "messageNone": "No content to display",
"unreadMessages": {
"one": "{} unread message",
"other": "{} unread messages"
},
"chatBreakNone": "None",
"settingsRealmCompactView": "Compact Realm View",
"settingsMixedFeed": "Mixed Feed", "settingsMixedFeed": "Mixed Feed",
"settingsAutoTranslate": "Auto Translate", "settingsAutoTranslate": "Auto Translate",
"settingsHideBottomNav": "Hide Bottom Navigation", "settingsHideBottomNav": "Hide Bottom Navigation",
@ -346,6 +379,7 @@
"postVisibilityUnlisted": "Unlisted", "postVisibilityUnlisted": "Unlisted",
"postVisibilityPrivate": "Private", "postVisibilityPrivate": "Private",
"postTruncated": "Content truncated, tap to view full post", "postTruncated": "Content truncated, tap to view full post",
"copyMessage": "Copy Message",
"authFactor": "Authentication Factor", "authFactor": "Authentication Factor",
"authFactorDelete": "Delete the Factor", "authFactorDelete": "Delete the Factor",
"authFactorDeleteHint": "Are you sure you want to delete this authentication factor? This action cannot be undone.", "authFactorDeleteHint": "Are you sure you want to delete this authentication factor? This action cannot be undone.",
@ -373,6 +407,10 @@
"lastActiveAt": "Last active at {}", "lastActiveAt": "Last active at {}",
"authDeviceLogout": "Logout", "authDeviceLogout": "Logout",
"authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.", "authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.",
"typingHint": {
"one": "{} is typing...",
"other": "{} are typing..."
},
"authDeviceEditLabel": "Edit Label", "authDeviceEditLabel": "Edit Label",
"authDeviceLabelTitle": "Edit Device Label", "authDeviceLabelTitle": "Edit Device Label",
"authDeviceLabelHint": "Enter a name for this device", "authDeviceLabelHint": "Enter a name for this device",
@ -439,6 +477,21 @@
"contactMethodSetPrimary": "Set as Primary", "contactMethodSetPrimary": "Set as Primary",
"contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications", "contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications",
"contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone.", "contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone.",
"chatNotifyLevel": "Notify Level",
"chatNotifyLevelDescription": "Decide how many notifications you will receive.",
"chatNotifyLevelAll": "All",
"chatNotifyLevelMention": "Mentions",
"chatNotifyLevelNone": "None",
"chatNotifyLevelUpdated": "The notify level has been updated to {}.",
"chatBreak": "Take a Break",
"chatBreakDescription": "Set a time, before that time, your notification level will be metions only, to take a break of the current topic they're talking about.",
"chatBreakClear": "Clear the break time",
"chatBreakHour": "{} break",
"chatBreakDay": "{} day break",
"chatBreakSet": "Break set for {}",
"chatBreakCleared": "Chat break has been cleared.",
"chatBreakCustom": "Custom duration",
"chatBreakEnterMinutes": "Enter minutes",
"firstName": "First Name", "firstName": "First Name",
"middleName": "Middle Name", "middleName": "Middle Name",
"lastName": "Last Name", "lastName": "Last Name",
@ -520,17 +573,29 @@
"quickActions": "Quick Actions", "quickActions": "Quick Actions",
"post": "Post", "post": "Post",
"copy": "Copy", "copy": "Copy",
"sendToChat": "Send to Chat",
"failedToShareToPost": "Failed to share to post: {}", "failedToShareToPost": "Failed to share to post: {}",
"shareToChatComingSoon": "Share to chat functionality coming soon", "shareToChatComingSoon": "Share to chat functionality coming soon",
"failedToShareToChat": "Failed to share to chat: {}",
"shareToSpecificChatComingSoon": "Share to {} coming soon",
"directChat": "Direct Chat",
"systemShareComingSoon": "System share functionality coming soon", "systemShareComingSoon": "System share functionality coming soon",
"failedToShareToSystem": "Failed to share to system: {}", "failedToShareToSystem": "Failed to share to system: {}",
"failedToCopy": "Failed to copy: {}", "failedToCopy": "Failed to copy: {}",
"noChatRoomsAvailable": "No chat rooms available",
"failedToLoadChats": "Failed to load chats",
"contentToShare": "Content to share:", "contentToShare": "Content to share:",
"unknownChat": "Unknown Chat",
"addAdditionalMessage": "Add additional message...",
"uploadingFiles": "Uploading files...", "uploadingFiles": "Uploading files...",
"sharedSuccessfully": "Shared successfully!",
"shareSuccess": "Shared successfully!", "shareSuccess": "Shared successfully!",
"shareToSpecificChatSuccess": "Shared to {} successfully!",
"wouldYouLikeToGoToChat": "Would you like to go to the chat?", "wouldYouLikeToGoToChat": "Would you like to go to the chat?",
"no": "No", "no": "No",
"yes": "Yes", "yes": "Yes",
"navigateToChat": "Navigate to Chat",
"wouldYouLikeToNavigateToChat": "Would you like to navigate to the chat?",
"abuseReport": "Report", "abuseReport": "Report",
"abuseReportTitle": "Report Content", "abuseReportTitle": "Report Content",
"abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.", "abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.",

View File

@ -7,6 +7,7 @@ import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker_android/image_picker_android.dart'; import 'package:image_picker_android/image_picker_android.dart';
@ -158,6 +159,28 @@ class IslandApp extends HookConsumerWidget {
} }
useEffect(() { useEffect(() {
const channel = MethodChannel('dev.solsynth.solian/notifications');
Future<void> handleInitialLink() async {
final String? link = await channel.invokeMethod('initialLink');
if (link != null) {
final router = ref.read(routerProvider);
router.go(link);
}
}
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
handleInitialLink();
}
channel.setMethodCallHandler((call) async {
if (call.method == 'newLink') {
final String link = call.arguments;
final router = ref.read(routerProvider);
router.go(link);
}
});
// When the app is opened from a terminated state. // When the app is opened from a terminated state.
FirebaseMessaging.instance.getInitialMessage().then((message) { FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message != null) { if (message != null) {

View File

@ -79,33 +79,37 @@ final routerProvider = Provider<GoRouter>((ref) {
return EventCalanderScreen(name: name); return EventCalanderScreen(name: name);
}, },
), ),
GoRoute( ShellRoute(
path: '/creators', builder:
builder: (context, state) => const CreatorHubScreen(), (context, state, child) => CreatorHubShellScreen(child: child),
routes: [ routes: [
GoRoute( GoRoute(
path: ':name/posts', path: '/creators',
builder: (context, state) => const CreatorHubScreen(),
),
GoRoute(
path: '/creators/:name/posts',
builder: (context, state) { builder: (context, state) {
final name = state.pathParameters['name']!; final name = state.pathParameters['name']!;
return CreatorPostListScreen(pubName: name); return CreatorPostListScreen(pubName: name);
}, },
), ),
GoRoute( GoRoute(
path: ':name/stickers', path: '/creators/:name/stickers',
builder: (context, state) { builder: (context, state) {
final name = state.pathParameters['name']!; final name = state.pathParameters['name']!;
return StickersScreen(pubName: name); return StickersScreen(pubName: name);
}, },
), ),
GoRoute( GoRoute(
path: ':name/stickers/new', path: '/creators/:name/stickers/new',
builder: (context, state) { builder: (context, state) {
final name = state.pathParameters['name']!; final name = state.pathParameters['name']!;
return NewStickerPacksScreen(pubName: name); return NewStickerPacksScreen(pubName: name);
}, },
), ),
GoRoute( GoRoute(
path: ':name/stickers/:packId/edit', path: '/creators/:name/stickers/:packId/edit',
builder: (context, state) { builder: (context, state) {
final name = state.pathParameters['name']!; final name = state.pathParameters['name']!;
final packId = state.pathParameters['packId']!; final packId = state.pathParameters['packId']!;
@ -113,7 +117,7 @@ final routerProvider = Provider<GoRouter>((ref) {
}, },
), ),
GoRoute( GoRoute(
path: ':name/stickers/:packId', path: '/creators/:name/stickers/:packId',
builder: (context, state) { builder: (context, state) {
final name = state.pathParameters['name']!; final name = state.pathParameters['name']!;
final packId = state.pathParameters['packId']!; final packId = state.pathParameters['packId']!;
@ -121,14 +125,14 @@ final routerProvider = Provider<GoRouter>((ref) {
}, },
), ),
GoRoute( GoRoute(
path: ':name/stickers/:packId/new', path: '/creators/:name/stickers/:packId/new',
builder: (context, state) { builder: (context, state) {
final packId = state.pathParameters['packId']!; final packId = state.pathParameters['packId']!;
return NewStickersScreen(packId: packId); return NewStickersScreen(packId: packId);
}, },
), ),
GoRoute( GoRoute(
path: ':name/stickers/:packId/:id/edit', path: '/creators/:name/stickers/:packId/:id/edit',
builder: (context, state) { builder: (context, state) {
final packId = state.pathParameters['packId']!; final packId = state.pathParameters['packId']!;
final id = state.pathParameters['id']!; final id = state.pathParameters['id']!;
@ -136,11 +140,11 @@ final routerProvider = Provider<GoRouter>((ref) {
}, },
), ),
GoRoute( GoRoute(
path: 'new', path: '/creators/new',
builder: (context, state) => const NewPublisherScreen(), builder: (context, state) => const NewPublisherScreen(),
), ),
GoRoute( GoRoute(
path: ':name/edit', path: '/creators/:name/edit',
builder: (context, state) { builder: (context, state) {
final name = state.pathParameters['name']!; final name = state.pathParameters['name']!;
return EditPublisherScreen(name: name); return EditPublisherScreen(name: name);
@ -173,56 +177,64 @@ final routerProvider = Provider<GoRouter>((ref) {
}, },
routes: [ routes: [
// Explore tab // Explore tab
GoRoute( ShellRoute(
path: '/', builder:
builder: (context, state) => const ExploreScreen(), (context, state, child) => ExploreShellScreen(child: child),
routes: [ routes: [
GoRoute( GoRoute(
path: 'posts/:id', path: '/',
builder: (context, state) => const ExploreScreen(),
),
GoRoute(
path: '/posts/:id',
builder: (context, state) { builder: (context, state) {
final id = state.pathParameters['id']!; final id = state.pathParameters['id']!;
return PostDetailScreen(id: id); return PostDetailScreen(id: id);
}, },
), ),
GoRoute( GoRoute(
path: 'publishers/:name', path: '/publishers/:name',
builder: (context, state) { builder: (context, state) {
final name = state.pathParameters['name']!; final name = state.pathParameters['name']!;
return PublisherProfileScreen(name: name); return PublisherProfileScreen(name: name);
}, },
), ),
GoRoute( GoRoute(
path: 'discovery/realms', path: '/discovery/realms',
builder: (context, state) => const DiscoveryRealmsScreen(), builder: (context, state) => const DiscoveryRealmsScreen(),
), ),
], ],
), ),
// Chat tab // Chat tab
GoRoute( ShellRoute(
path: '/chat', builder:
builder: (context, state) => const ChatListScreen(), (context, state, child) => ChatShellScreen(child: child),
routes: [ routes: [
GoRoute( GoRoute(
path: 'new', path: '/chat',
builder: (context, state) => const ChatListScreen(),
),
GoRoute(
path: '/chat/new',
builder: (context, state) => const NewChatScreen(), builder: (context, state) => const NewChatScreen(),
), ),
GoRoute( GoRoute(
path: ':id', path: '/chat/:id',
builder: (context, state) { builder: (context, state) {
final id = state.pathParameters['id']!; final id = state.pathParameters['id']!;
return ChatRoomScreen(id: id); return ChatRoomScreen(id: id);
}, },
), ),
GoRoute( GoRoute(
path: ':id/edit', path: '/chat/:id/edit',
builder: (context, state) { builder: (context, state) {
final id = state.pathParameters['id']!; final id = state.pathParameters['id']!;
return EditChatScreen(id: id); return EditChatScreen(id: id);
}, },
), ),
GoRoute( GoRoute(
path: ':id/detail', path: '/chat/:id/detail',
builder: (context, state) { builder: (context, state) {
final id = state.pathParameters['id']!; final id = state.pathParameters['id']!;
return ChatDetailScreen(id: id); return ChatDetailScreen(id: id);
@ -258,39 +270,43 @@ final routerProvider = Provider<GoRouter>((ref) {
), ),
// Account tab // Account tab
GoRoute( ShellRoute(
path: '/account', builder:
builder: (context, state) => const AccountScreen(), (context, state, child) => AccountShellScreen(child: child),
routes: [ routes: [
GoRoute( GoRoute(
path: 'notifications', path: '/account',
builder: (context, state) => const AccountScreen(),
),
GoRoute(
path: '/account/notifications',
builder: (context, state) => const NotificationScreen(), builder: (context, state) => const NotificationScreen(),
), ),
GoRoute( GoRoute(
path: 'wallet', path: '/account/wallet',
builder: (context, state) => const WalletScreen(), builder: (context, state) => const WalletScreen(),
), ),
GoRoute( GoRoute(
path: 'relationships', path: '/account/relationships',
builder: (context, state) => const RelationshipScreen(), builder: (context, state) => const RelationshipScreen(),
), ),
GoRoute( GoRoute(
path: ':name', path: '/account/:name',
builder: (context, state) { builder: (context, state) {
final name = state.pathParameters['name']!; final name = state.pathParameters['name']!;
return AccountProfileScreen(name: name); return AccountProfileScreen(name: name);
}, },
), ),
GoRoute( GoRoute(
path: 'me/update', path: '/account/me/update',
builder: (context, state) => const UpdateProfileScreen(), builder: (context, state) => const UpdateProfileScreen(),
), ),
GoRoute( GoRoute(
path: 'me/leveling', path: '/account/me/leveling',
builder: (context, state) => const LevelingScreen(), builder: (context, state) => const LevelingScreen(),
), ),
GoRoute( GoRoute(
path: 'settings', path: '/account/settings',
builder: (context, state) => const AccountSettingsScreen(), builder: (context, state) => const AccountSettingsScreen(),
), ),
], ],

View File

@ -143,7 +143,7 @@ class AccountScreen extends HookConsumerWidget {
progress: user.value!.profile.levelingProgress, progress: user.value!.profile.levelingProgress,
), ),
onTap: () { onTap: () {
context.push('/account/leveling'); context.push('/account/me/leveling');
}, },
).padding(horizontal: 12), ).padding(horizontal: 12),
Row( Row(
@ -210,7 +210,7 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24), contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('wallet').tr(), title: Text('wallet').tr(),
onTap: () { onTap: () {
context.push('/wallet'); context.push('/account/wallet');
}, },
), ),
ListTile( ListTile(

View File

@ -53,17 +53,21 @@ Future<List<SnAccountBadge>> accountBadges(Ref ref, String uname) async {
@riverpod @riverpod
Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async { Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
final account = await ref.watch(accountProvider(uname).future); try {
if (account.profile.background == null) return null; final account = await ref.watch(accountProvider(uname).future);
final palette = await PaletteGenerator.fromImageProvider( if (account.profile.background == null) return null;
CloudImageWidget.provider( final palette = await PaletteGenerator.fromImageProvider(
fileId: account.profile.background!.id, CloudImageWidget.provider(
serverUrl: ref.watch(serverUrlProvider), fileId: account.profile.background!.id,
), serverUrl: ref.watch(serverUrlProvider),
); ),
final dominantColor = palette.dominantColor?.color; );
if (dominantColor == null) return null; final dominantColor = palette.dominantColor?.color;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
} catch (_) {
return null;
}
} }
@riverpod @riverpod

View File

@ -4,19 +4,18 @@ import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart'; 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:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart'; import 'package:island/models/user.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/route.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/markdown.dart'; import 'package:island/widgets/content/markdown.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart';
part 'notification.g.dart'; part 'notification.g.dart';
@ -180,36 +179,17 @@ class NotificationScreen extends HookConsumerWidget {
), ),
), ),
onTap: () { onTap: () {
if (notification.meta['link'] is String) { if (notification.meta['action_uri'] != null) {
final href = notification.meta['link']; var uri = notification.meta['action_uri'] as String;
final uri = Uri.tryParse(href); if (uri.startsWith('/')) {
if (uri == null) { // In-app routes
showSnackBar( rootNavigatorKey.currentContext?.push(
'brokenLink'.tr(args: []), notification.meta['action_uri'],
action: SnackBarAction(
label: 'copyToClipboard'.tr(),
onPressed: () {
Clipboard.setData(ClipboardData(text: href));
clearSnackBar(context);
},
),
); );
return; } else {
// External URLs
launchUrlString(uri);
} }
if (uri.scheme == 'solian') {
context.push(
['', uri.host, ...uri.pathSegments].join('/'),
);
return;
}
showConfirmAlert(
'openLinkConfirmDescription'.tr(args: [href]),
'openLinkConfirm'.tr(),
).then((value) {
if (value) {
launchUrl(uri, mode: LaunchMode.externalApplication);
}
});
} }
}, },
); );