Compare commits

...

25 Commits

Author SHA1 Message Date
a7454edec0 🗑️ Remove unused code 2025-07-12 02:01:36 +08:00
cbf1952eb7 🐛 Fix rotation 2025-07-11 00:53:36 +08:00
6d06f0a1b4 🐛 Fix bugs and don't know what has been fixed 2025-07-11 00:44:08 +08:00
f2d2a9efd8 👽 Update API references 2025-07-10 16:43:20 +08:00
d44c8217b0 🐛 Bug fixes and support new version of API 2025-07-10 16:40:43 +08:00
446c33d8b0 🐛 Fix wrong route path 2025-07-06 12:28:42 +08:00
996462f1fd 🚀 Launch 3.0.0+112 2025-07-03 22:25:02 +08:00
778f6bb79f Android deeplinking 2025-07-03 22:18:02 +08:00
8747f948b9 Apple deeplinking 2025-07-03 22:14:34 +08:00
9546d6e4b8 🗑️ Disable the technical review tour 2025-07-03 22:11:12 +08:00
f8d1940af6 Report user 2025-07-03 22:07:51 +08:00
b2b0891d24 Block user 2025-07-03 22:03:12 +08:00
274168d4bc 💄 Better abuse reports 2025-07-03 21:31:37 +08:00
2c98b348d5 Abuse reports 2025-07-03 20:53:30 +08:00
afc7887ddd 🐛 Fix delete post on explore didn't refresh 2025-07-03 19:08:56 +08:00
99ff78a3d5 🐛 Fix post item padding 2025-07-03 13:12:39 +08:00
2ad85addf6 🐛 Fix wrong notification permission is requested 2025-07-03 01:29:43 +08:00
552b4b2572 🚀 Launch 3.0.0+111 2025-07-03 01:20:54 +08:00
594ac39e3d Article attachments 2025-07-03 01:18:07 +08:00
23321171f3 💄 Optimized attachment insert in article compose 2025-07-03 01:11:56 +08:00
ee72d79c93 Hide attachments list on article 2025-07-03 01:06:01 +08:00
a20c2598fc 🐛 Fixes render errors of unauthorized 2025-07-03 00:49:11 +08:00
2eba871a6d 💄 Web article has max width 2025-07-03 00:39:45 +08:00
46919dec31 📝 Updated about screen 2025-07-03 00:34:11 +08:00
9dd6cffe0c 🐛 Trying to fix push notification 2025-07-03 00:27:30 +08:00
55 changed files with 1333 additions and 338 deletions

View File

@ -41,7 +41,16 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<!-- Deeplinking -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="solian.app" />
<data android:scheme="https" />
</intent-filter>
<!-- Share Intent Filters --> <!-- Share Intent Filters -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />

View File

@ -375,7 +375,9 @@
"postContent": "Content", "postContent": "Content",
"postSettings": "Settings", "postSettings": "Settings",
"postPublisherUnselected": "Publisher Unspecified", "postPublisherUnselected": "Publisher Unspecified",
"postVisibility": "Visibility", "postType": "Post Type",
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
"postVisibility": "Post Visibility",
"postVisibilityPublic": "Public", "postVisibilityPublic": "Public",
"postVisibilityFriends": "Friends Only", "postVisibilityFriends": "Friends Only",
"postVisibilityUnlisted": "Unlisted", "postVisibilityUnlisted": "Unlisted",
@ -588,6 +590,7 @@
"yes": "Yes", "yes": "Yes",
"navigateToChat": "Navigate to Chat", "navigateToChat": "Navigate to Chat",
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?", "wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
"abuseReports": "Abuse Reports",
"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.",
@ -686,8 +689,9 @@
"aboutScreenDeveloperSectionTitle": "Developer", "aboutScreenDeveloperSectionTitle": "Developer",
"aboutScreenContactUsTitle": "Contact Us", "aboutScreenContactUsTitle": "Contact Us",
"aboutScreenLicenseTitle": "License", "aboutScreenLicenseTitle": "License",
"aboutScreenLicenseContent": "All copyright reserved © {} Solsynth\nOpen-sourced under license GNU AGPL v3.0", "aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
"aboutScreenCopyright": "© {} {}. All rights reserved.", "aboutScreenCopyright": "All rights reserved © Solsynth {}",
"aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team",
"aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}", "aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}",
"copiedToClipboard": "Copied to clipboard", "copiedToClipboard": "Copied to clipboard",
"copyToClipboardTooltip": "Copy to clipboard", "copyToClipboardTooltip": "Copy to clipboard",

View File

@ -512,5 +512,33 @@
"orderId": "订单 ID", "orderId": "订单 ID",
"enterOrderId": "输入您的订单 ID", "enterOrderId": "输入您的订单 ID",
"restore": "恢复", "restore": "恢复",
"keyboardShortcuts": "键盘快捷键" "keyboardShortcuts": "键盘快捷键",
"about": "关于",
"membershipCancel": "取消会员订阅",
"membershipCancelConfirm": "您确定要取消您的会员订阅?",
"membershipCancelHint": "您确定要取消您的会员订阅吗?您将不会再被收费。您的会员资格将在当前计费周期结束前保持有效。并且您在当前订阅结束之前无法重新订阅。",
"membershipCancelSuccess": "您的会员订阅已成功取消。",
"aboutScreenTitle": "关于",
"aboutScreenVersionInfo": "版本 {} ({})",
"aboutScreenAppInfoSectionTitle": "应用信息",
"aboutScreenPackageNameLabel": "包名",
"aboutScreenVersionLabel": "版本",
"aboutScreenBuildNumberLabel": "构建编号",
"aboutScreenLinksSectionTitle": "链接",
"aboutScreenPrivacyPolicyTitle": "隐私政策",
"aboutScreenTermsOfServiceTitle": "服务条款",
"aboutScreenOpenSourceLicensesTitle": "开源许可证",
"aboutScreenDeveloperSectionTitle": "开发者",
"aboutScreenContactUsTitle": "联系我们",
"aboutScreenLicenseTitle": "许可证",
"aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
"aboutScreenCopyright": "版权所有 © 索尔辛茨 {}",
"aboutScreenMadeWith": "由 Solar Network Team 用 ❤︎️ 制作",
"aboutScreenFailedToLoadPackageInfo": "加载包信息失败:{error}",
"copiedToClipboard": "已复制到剪贴板",
"copyToClipboardTooltip": "复制到剪贴板",
"postForwardingTo": "转发给",
"postReplyingTo": "回复给",
"postEditing": "您正在编辑现有帖子",
"postArticle": "文章"
} }

View File

@ -45,10 +45,10 @@ PODS:
- Firebase/Messaging (11.15.0): - Firebase/Messaging (11.15.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 11.15.0) - FirebaseMessaging (~> 11.15.0)
- firebase_core (3.15.0): - firebase_core (3.15.1):
- Firebase/CoreOnly (= 11.15.0) - Firebase/CoreOnly (= 11.15.0)
- Flutter - Flutter
- firebase_messaging (15.2.8): - firebase_messaging (15.2.9):
- Firebase/Messaging (= 11.15.0) - Firebase/Messaging (= 11.15.0)
- firebase_core - firebase_core
- Flutter - Flutter
@ -130,7 +130,7 @@ PODS:
- Flutter - Flutter
- irondash_engine_context (0.0.1): - irondash_engine_context (0.0.1):
- Flutter - Flutter
- Kingfisher (8.3.3) - Kingfisher (8.4.0)
- livekit_client (2.4.9): - livekit_client (2.4.9):
- Flutter - Flutter
- flutter_webrtc - flutter_webrtc
@ -178,18 +178,18 @@ PODS:
- sqflite_darwin (0.0.4): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (3.50.1): - sqlite3 (3.50.2):
- sqlite3/common (= 3.50.1) - sqlite3/common (= 3.50.2)
- sqlite3/common (3.50.1) - sqlite3/common (3.50.2)
- sqlite3/dbstatvtab (3.50.1): - sqlite3/dbstatvtab (3.50.2):
- sqlite3/common - sqlite3/common
- sqlite3/fts5 (3.50.1): - sqlite3/fts5 (3.50.2):
- sqlite3/common - sqlite3/common
- sqlite3/math (3.50.1): - sqlite3/math (3.50.2):
- sqlite3/common - sqlite3/common
- sqlite3/perf-threadsafe (3.50.1): - sqlite3/perf-threadsafe (3.50.2):
- sqlite3/common - sqlite3/common
- sqlite3/rtree (3.50.1): - sqlite3/rtree (3.50.2):
- sqlite3/common - sqlite3/common
- sqlite3_flutter_libs (0.0.1): - sqlite3_flutter_libs (0.0.1):
- Flutter - Flutter
@ -362,8 +362,8 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_core: c727a02c560a53f1f1e56e18f16515eb5753c492 firebase_core: ece862f94b2bc72ee0edbeec7ab5c7cb09fe1ab5
firebase_messaging: 4158969b04b667f5435731ec9d6e453bb58b0c4c firebase_messaging: e1a5fae495603115be1d0183bc849da748734e2b
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843 FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
@ -382,9 +382,9 @@ SPEC CHECKSUMS:
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
Kingfisher: ff82cb91d9266ddb56cbb2f72d32c26f00d3e5be Kingfisher: b14cc47bbfa7a3c150dd12962ee9c86338545629
livekit_client: 3f79d79233a5bd13d5b541732624ef959d7c538e livekit_client: 3f79d79233a5bd13d5b541732624ef959d7c538e
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
@ -403,7 +403,7 @@ SPEC CHECKSUMS:
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 1d85290c3321153511f6e900ede7a1608718bbd5 sqlite3: 3e82a2daae39ba3b41ae6ee84a130494585460fc
sqlite3_flutter_libs: e7fc8c9ea2200ff3271f08f127842131746b70e2 sqlite3_flutter_libs: e7fc8c9ea2200ff3271f08f127842131746b70e2
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4

View File

@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 54; objectVersion = 77;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@ -525,10 +525,14 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@ -586,10 +590,14 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@ -772,6 +780,7 @@
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian; INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -1202,6 +1211,7 @@
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian; INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -1229,6 +1239,7 @@
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian; INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",

View File

@ -27,6 +27,7 @@ import UIKit
UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory]) UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory])
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
} }

View File

@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Flutter View Controller--> <!--Flutter View Controller-->
@ -14,13 +16,14 @@
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/> <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides> </layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC"> <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/> <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view> </view>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="-26" y="-76"/>
</scene> </scene>
</scenes> </scenes>
</document> </document>

View File

@ -2,16 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CLIENT_ID</key> <key>AppGroupId</key>
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string> <string>$(CUSTOM_GROUP_ID)</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key> <key>BUNDLE_ID</key>
<string>dev.solsynth.solian</string> <string>dev.solsynth.solian</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
@ -32,31 +26,46 @@
<string>$(FLUTTER_BUILD_NAME)</string> <string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>CFBundleURLTypes</key> <key>CLIENT_ID</key>
<array> <string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
<dict> <key>ITSAppUsesNonExemptEncryption</key>
<key>CFBundleTypeRole</key> <false/>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSCalendarsUsageDescription</key> <key>NSCalendarsUsageDescription</key>
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string> <string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Grant access to Camera will allow Solian take photo or video for your post.</string> <string>Grant access to Camera will allow Solian take photo or video for your post.</string>
<key>NSFaceIDUsageDescription</key>
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>Grant access to Microphone will allow Solian record audio for your post.</string> <string>Grant access to Microphone will allow Solian record audio for your post.</string>
<key>NSPhotoLibraryAddUsageDescription</key> <key>NSPhotoLibraryAddUsageDescription</key>
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string> <string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string> <string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
<key>NSUserActivityTypes</key>
<array>
<string>INStartCallIntent</string>
<string>INSendMessageIntent</string>
</array>
<key>PLIST_VERSION</key>
<string>1</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
@ -74,25 +83,16 @@
<false/> <false/>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> <string>UIInterfaceOrientationPortrait</string>
<key>NSFaceIDUsageDescription</key> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>NSUserActivityTypes</key>
<array>
<string>INStartCallIntent</string>
<string>INSendMessageIntent</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@ -221,7 +221,7 @@ class IslandApp extends HookConsumerWidget {
Future(() { Future(() {
userNotifier.fetchUser().then((_) { userNotifier.fetchUser().then((_) {
final user = ref.watch(userInfoProvider); final user = ref.watch(userInfoProvider);
if (user.hasValue) { if (user.value != null) {
final apiClient = ref.read(apiClientProvider); final apiClient = ref.read(apiClientProvider);
subscribePushNotification(apiClient); subscribePushNotification(apiClient);
final wsNotifier = ref.read(websocketStateProvider.notifier); final wsNotifier = ref.read(websocketStateProvider.notifier);

View File

@ -0,0 +1,23 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'abuse_report.freezed.dart';
part 'abuse_report.g.dart';
@freezed
sealed class SnAbuseReport with _$SnAbuseReport {
const factory SnAbuseReport({
required String id,
required String resourceIdentifier,
required int type,
required String reason,
required DateTime? resolvedAt,
required String? resolution,
required String accountId,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnAbuseReport;
factory SnAbuseReport.fromJson(Map<String, dynamic> json) =>
_$SnAbuseReportFromJson(json);
}

View File

@ -0,0 +1,175 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'abuse_report.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnAbuseReport {
String get id; String get resourceIdentifier; int get type; String get reason; DateTime? get resolvedAt; String? get resolution; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAbuseReportCopyWith<SnAbuseReport> get copyWith => _$SnAbuseReportCopyWithImpl<SnAbuseReport>(this as SnAbuseReport, _$identity);
/// Serializes this SnAbuseReport to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAbuseReport&&(identical(other.id, id) || other.id == id)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.type, type) || other.type == type)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.resolvedAt, resolvedAt) || other.resolvedAt == resolvedAt)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,resourceIdentifier,type,reason,resolvedAt,resolution,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAbuseReport(id: $id, resourceIdentifier: $resourceIdentifier, type: $type, reason: $reason, resolvedAt: $resolvedAt, resolution: $resolution, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnAbuseReportCopyWith<$Res> {
factory $SnAbuseReportCopyWith(SnAbuseReport value, $Res Function(SnAbuseReport) _then) = _$SnAbuseReportCopyWithImpl;
@useResult
$Res call({
String id, String resourceIdentifier, int type, String reason, DateTime? resolvedAt, String? resolution, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnAbuseReportCopyWithImpl<$Res>
implements $SnAbuseReportCopyWith<$Res> {
_$SnAbuseReportCopyWithImpl(this._self, this._then);
final SnAbuseReport _self;
final $Res Function(SnAbuseReport) _then;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? resourceIdentifier = null,Object? type = null,Object? reason = null,Object? resolvedAt = freezed,Object? resolution = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as String,resolvedAt: freezed == resolvedAt ? _self.resolvedAt : resolvedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,resolution: freezed == resolution ? _self.resolution : resolution // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnAbuseReport implements SnAbuseReport {
const _SnAbuseReport({required this.id, required this.resourceIdentifier, required this.type, required this.reason, required this.resolvedAt, required this.resolution, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnAbuseReport.fromJson(Map<String, dynamic> json) => _$SnAbuseReportFromJson(json);
@override final String id;
@override final String resourceIdentifier;
@override final int type;
@override final String reason;
@override final DateTime? resolvedAt;
@override final String? resolution;
@override final String accountId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAbuseReportCopyWith<_SnAbuseReport> get copyWith => __$SnAbuseReportCopyWithImpl<_SnAbuseReport>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAbuseReportToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAbuseReport&&(identical(other.id, id) || other.id == id)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.type, type) || other.type == type)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.resolvedAt, resolvedAt) || other.resolvedAt == resolvedAt)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,resourceIdentifier,type,reason,resolvedAt,resolution,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAbuseReport(id: $id, resourceIdentifier: $resourceIdentifier, type: $type, reason: $reason, resolvedAt: $resolvedAt, resolution: $resolution, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnAbuseReportCopyWith<$Res> implements $SnAbuseReportCopyWith<$Res> {
factory _$SnAbuseReportCopyWith(_SnAbuseReport value, $Res Function(_SnAbuseReport) _then) = __$SnAbuseReportCopyWithImpl;
@override @useResult
$Res call({
String id, String resourceIdentifier, int type, String reason, DateTime? resolvedAt, String? resolution, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnAbuseReportCopyWithImpl<$Res>
implements _$SnAbuseReportCopyWith<$Res> {
__$SnAbuseReportCopyWithImpl(this._self, this._then);
final _SnAbuseReport _self;
final $Res Function(_SnAbuseReport) _then;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? resourceIdentifier = null,Object? type = null,Object? reason = null,Object? resolvedAt = freezed,Object? resolution = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAbuseReport(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as String,resolvedAt: freezed == resolvedAt ? _self.resolvedAt : resolvedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,resolution: freezed == resolution ? _self.resolution : resolution // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
// dart format on

View File

@ -0,0 +1,41 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'abuse_report.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SnAbuseReport _$SnAbuseReportFromJson(Map<String, dynamic> json) =>
_SnAbuseReport(
id: json['id'] as String,
resourceIdentifier: json['resource_identifier'] as String,
type: (json['type'] as num).toInt(),
reason: json['reason'] as String,
resolvedAt:
json['resolved_at'] == null
? null
: DateTime.parse(json['resolved_at'] as String),
resolution: json['resolution'] as String?,
accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnAbuseReportToJson(_SnAbuseReport instance) =>
<String, dynamic>{
'id': instance.id,
'resource_identifier': instance.resourceIdentifier,
'type': instance.type,
'reason': instance.reason,
'resolved_at': instance.resolvedAt?.toIso8601String(),
'resolution': instance.resolution,
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@ -0,0 +1,38 @@
enum AbuseReportType {
copyright(0),
harassment(1),
impersonation(2),
offensiveContent(3),
spam(4),
privacyViolation(5),
illegalContent(6),
other(7);
const AbuseReportType(this.value);
final int value;
static AbuseReportType fromValue(int value) {
return values.firstWhere((e) => e.value == value);
}
String get displayName {
switch (this) {
case AbuseReportType.copyright:
return 'Copyright';
case AbuseReportType.harassment:
return 'Harassment';
case AbuseReportType.impersonation:
return 'Impersonation';
case AbuseReportType.offensiveContent:
return 'Offensive Content';
case AbuseReportType.spam:
return 'Spam';
case AbuseReportType.privacyViolation:
return 'Privacy Violation';
case AbuseReportType.illegalContent:
return 'Illegal Content';
case AbuseReportType.other:
return 'Other';
}
}
}

View File

@ -65,12 +65,13 @@ final apiClientProvider = Provider<Dio>((ref) {
final serverUrl = ref.watch(serverUrlProvider); final serverUrl = ref.watch(serverUrlProvider);
final dio = Dio( final dio = Dio(
BaseOptions( BaseOptions(
baseUrl: serverUrl, baseUrl: '$serverUrl/api',
connectTimeout: const Duration(seconds: 10), connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10),
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Client': 'Solian',
}, },
), ),
); );

View File

@ -18,8 +18,13 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
final user = SnAccount.fromJson(response.data); final user = SnAccount.fromJson(response.data);
state = AsyncValue.data(user); state = AsyncValue.data(user);
} catch (error, stackTrace) { } catch (error, stackTrace) {
log("[UserInfo] Failed to fetch user info: $error"); log(
state = AsyncValue.error(error, stackTrace); "[UserInfo] Failed to fetch user info...",
name: 'UserInfoNotifier',
error: error,
stackTrace: stackTrace,
);
state = AsyncValue.data(null);
} }
} }

View File

@ -11,7 +11,7 @@ import 'package:island/screens/posts/post_search.dart';
import 'package:island/widgets/app_wrapper.dart'; import 'package:island/widgets/app_wrapper.dart';
import 'package:island/screens/tabs.dart'; import 'package:island/screens/tabs.dart';
import 'package:island/screens/explore.dart'; import 'package:island/screens/explore.dart';
import 'package:island/screens/article_detail_screen.dart'; import 'package:island/screens/discovery/article_detail.dart';
import 'package:island/screens/account.dart'; import 'package:island/screens/account.dart';
import 'package:island/screens/notification.dart'; import 'package:island/screens/notification.dart';
import 'package:island/screens/wallet.dart'; import 'package:island/screens/wallet.dart';
@ -41,6 +41,8 @@ import 'package:island/screens/realm/realms.dart';
import 'package:island/screens/realm/realm_detail.dart'; import 'package:island/screens/realm/realm_detail.dart';
import 'package:island/screens/account/event_calendar.dart'; import 'package:island/screens/account/event_calendar.dart';
import 'package:island/screens/discovery/realms.dart'; import 'package:island/screens/discovery/realms.dart';
import 'package:island/screens/reports/report_detail.dart';
import 'package:island/screens/reports/report_list.dart';
// Shell route keys for nested navigation // Shell route keys for nested navigation
final rootNavigatorKey = GlobalKey<NavigatorState>(); final rootNavigatorKey = GlobalKey<NavigatorState>();
@ -258,6 +260,19 @@ final routerProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const AboutScreen(), builder: (context, state) => const AboutScreen(),
), ),
GoRoute(
path: '/safety/reports/me',
builder: (context, state) => const AbuseReportListScreen(),
),
GoRoute(
path: '/safety/reports/me/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return AbuseReportDetailScreen(reportId: id);
},
),
// Main tabs with TabsScreen shell // Main tabs with TabsScreen shell
ShellRoute( ShellRoute(
navigatorKey: _tabsShellKey, navigatorKey: _tabsShellKey,
@ -399,7 +414,7 @@ final routerProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const LevelingScreen(), builder: (context, state) => const LevelingScreen(),
), ),
GoRoute( GoRoute(
path: '/account/settings', path: '/account/me/settings',
builder: (context, state) => const AccountSettingsScreen(), builder: (context, state) => const AccountSettingsScreen(),
), ),
], ],

View File

@ -1,24 +1,35 @@
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/notify.dart';
import 'package:island/services/udid.native.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.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.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
class AboutScreen extends StatefulWidget { class AboutScreen extends ConsumerStatefulWidget {
const AboutScreen({super.key}); const AboutScreen({super.key});
@override @override
State<AboutScreen> createState() => _AboutScreenState(); ConsumerState<AboutScreen> createState() => _AboutScreenState();
} }
class _AboutScreenState extends State<AboutScreen> { class _AboutScreenState extends ConsumerState<AboutScreen> {
PackageInfo _packageInfo = PackageInfo( PackageInfo _packageInfo = PackageInfo(
appName: 'Solian', appName: 'Solian',
packageName: 'dev.solsynth.solian', packageName: 'dev.solsynth.solian',
version: '1.0.0', version: '1.0.0',
buildNumber: '1', buildNumber: '1',
); );
BaseDeviceInfo? _deviceInfo;
String? _deviceUdid;
bool _isLoading = true; bool _isLoading = true;
String? _errorMessage; String? _errorMessage;
@ -26,6 +37,7 @@ class _AboutScreenState extends State<AboutScreen> {
void initState() { void initState() {
super.initState(); super.initState();
_initPackageInfo(); _initPackageInfo();
_initDeviceInfo();
} }
Future<void> _initPackageInfo() async { Future<void> _initPackageInfo() async {
@ -49,6 +61,25 @@ class _AboutScreenState extends State<AboutScreen> {
} }
} }
Future<void> _initDeviceInfo() async {
try {
final deviceInfoPlugin = DeviceInfoPlugin();
_deviceInfo = await deviceInfoPlugin.deviceInfo;
_deviceUdid = await getUdid();
if (mounted) {
setState(() {});
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = 'aboutScreenFailedToLoadDeviceInfo'.tr(
args: [e.toString()],
);
});
}
}
}
Future<void> _launchURL(String url) async { Future<void> _launchURL(String url) async {
final uri = Uri.parse(url); final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) { if (await canLaunchUrl(uri)) {
@ -60,7 +91,7 @@ class _AboutScreenState extends State<AboutScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Scaffold( return AppScaffold(
appBar: AppBar(title: Text('about'.tr()), elevation: 0), appBar: AppBar(title: Text('about'.tr()), elevation: 0),
body: body:
_isLoading _isLoading
@ -108,25 +139,66 @@ class _AboutScreenState extends State<AboutScreen> {
children: [ children: [
_buildInfoItem( _buildInfoItem(
context, context,
icon: Icons.info_outline, icon: Symbols.info,
label: 'aboutScreenPackageNameLabel'.tr(), label: 'aboutScreenPackageNameLabel'.tr(),
value: _packageInfo.packageName, value: _packageInfo.packageName,
), ),
_buildInfoItem( _buildInfoItem(
context, context,
icon: Icons.update, icon: Symbols.update,
label: 'aboutScreenVersionLabel'.tr(), label: 'aboutScreenVersionLabel'.tr(),
value: _packageInfo.version, value: _packageInfo.version,
), ),
_buildInfoItem( _buildInfoItem(
context, context,
icon: Icons.build, icon: Symbols.build,
label: 'aboutScreenBuildNumberLabel'.tr(), label: 'aboutScreenBuildNumberLabel'.tr(),
value: _packageInfo.buildNumber, value: _packageInfo.buildNumber,
), ),
], ],
), ),
if (_deviceInfo != null) const SizedBox(height: 16),
if (_deviceInfo != null)
_buildSection(
context,
title: 'Device Information',
children: [
_buildInfoItem(
context,
icon: Symbols.label,
label: 'Device Name',
value: _deviceInfo?.data['name'],
),
_buildInfoItem(
context,
icon: Symbols.fingerprint,
label: 'Device Identifier',
value: _deviceUdid ?? 'N/A',
copyable: true,
),
const Divider(height: 1),
_buildListTile(
context,
icon: Symbols.notifications_active,
title: 'Reactivate Push Notifications',
onTap: () async {
showLoadingModal(context);
try {
await subscribePushNotification(
ref.watch(apiClientProvider),
);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
},
),
],
),
const SizedBox(height: 16), const SizedBox(height: 16),
// Links Card // Links Card
@ -136,7 +208,7 @@ class _AboutScreenState extends State<AboutScreen> {
children: [ children: [
_buildListTile( _buildListTile(
context, context,
icon: Icons.privacy_tip_outlined, icon: Symbols.privacy_tip,
title: 'aboutScreenPrivacyPolicyTitle'.tr(), title: 'aboutScreenPrivacyPolicyTitle'.tr(),
onTap: onTap:
() => _launchURL( () => _launchURL(
@ -145,7 +217,7 @@ class _AboutScreenState extends State<AboutScreen> {
), ),
_buildListTile( _buildListTile(
context, context,
icon: Icons.description_outlined, icon: Symbols.description,
title: 'aboutScreenTermsOfServiceTitle'.tr(), title: 'aboutScreenTermsOfServiceTitle'.tr(),
onTap: onTap:
() => _launchURL( () => _launchURL(
@ -154,7 +226,7 @@ class _AboutScreenState extends State<AboutScreen> {
), ),
_buildListTile( _buildListTile(
context, context,
icon: Icons.code, icon: Symbols.code,
title: 'aboutScreenOpenSourceLicensesTitle'.tr(), title: 'aboutScreenOpenSourceLicensesTitle'.tr(),
onTap: () { onTap: () {
showLicensePage( showLicensePage(
@ -177,14 +249,14 @@ class _AboutScreenState extends State<AboutScreen> {
children: [ children: [
_buildListTile( _buildListTile(
context, context,
icon: Icons.email_outlined, icon: Symbols.email,
title: 'aboutScreenContactUsTitle'.tr(), title: 'aboutScreenContactUsTitle'.tr(),
subtitle: 'lily@solsynth.dev', subtitle: 'lily@solsynth.dev',
onTap: () => _launchURL('mailto:lily@solsynth.dev'), onTap: () => _launchURL('mailto:lily@solsynth.dev'),
), ),
_buildListTile( _buildListTile(
context, context,
icon: Icons.copyright, icon: Symbols.copyright,
title: 'aboutScreenLicenseTitle'.tr(), title: 'aboutScreenLicenseTitle'.tr(),
subtitle: 'aboutScreenLicenseContent'.tr( subtitle: 'aboutScreenLicenseContent'.tr(
args: [DateTime.now().year.toString()], args: [DateTime.now().year.toString()],
@ -202,14 +274,25 @@ class _AboutScreenState extends State<AboutScreen> {
// Copyright // Copyright
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Text( child: Column(
'aboutScreenCopyright'.tr( children: [
args: [DateTime.now().year.toString(), "Solsynth"], Text(
), 'aboutScreenCopyright'.tr(
style: theme.textTheme.bodySmall, args: [DateTime.now().year.toString()],
textAlign: TextAlign.center, ),
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
const Gap(1),
Text(
'aboutScreenMadeWith'.tr(),
textAlign: TextAlign.center,
).fontSize(10).opacity(0.8),
],
), ),
), ),
Gap(MediaQuery.of(context).padding.bottom + 16),
], ],
), ),
), ),
@ -247,6 +330,7 @@ class _AboutScreenState extends State<AboutScreen> {
required IconData icon, required IconData icon,
required String label, required String label,
required String value, required String value,
bool copyable = false,
}) { }) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
@ -263,13 +347,14 @@ class _AboutScreenState extends State<AboutScreen> {
SelectableText( SelectableText(
value, value,
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
maxLines: copyable ? 1 : null,
), ),
], ],
), ),
), ),
if (value.startsWith('http') || value.contains('@')) if (value.startsWith('http') || value.contains('@') || copyable)
IconButton( IconButton(
icon: const Icon(Icons.copy, size: 16), icon: const Icon(Symbols.content_copy, size: 16),
onPressed: () { onPressed: () {
Clipboard.setData(ClipboardData(text: value)); Clipboard.setData(ClipboardData(text: value));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -301,7 +386,7 @@ class _AboutScreenState extends State<AboutScreen> {
subtitle: subtitle != null ? Text(subtitle) : null, subtitle: subtitle != null ? Text(subtitle) : null,
isThreeLine: multipleLines, isThreeLine: multipleLines,
trailing: const Icon( trailing: const Icon(
Icons.chevron_right, Symbols.chevron_right,
).padding(top: multipleLines ? 8 : 0), ).padding(top: multipleLines ? 8 : 0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
onTap: onTap, onTap: onTap,

View File

@ -59,7 +59,7 @@ class AccountScreen extends HookConsumerWidget {
notificationUnreadCountNotifierProvider, notificationUnreadCountNotifierProvider,
); );
if (!user.hasValue || user.value == null) { if (user.value == null || user.value == null) {
return _UnauthorizedAccountScreen(); return _UnauthorizedAccountScreen();
} }
@ -222,9 +222,17 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24), contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('relationships').tr(), title: Text('relationships').tr(),
onTap: () { onTap: () {
context.push('/account/relationship'); context.push('/account/relationships');
}, },
), ),
ListTile(
minTileHeight: 48,
title: Text('abuseReports').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.gavel),
trailing: const Icon(Symbols.chevron_right),
onTap: () => context.push('/safety/reports/me'),
),
const Divider(height: 1).padding(vertical: 8), const Divider(height: 1).padding(vertical: 8),
ListTile( ListTile(
minTileHeight: 48, minTileHeight: 48,
@ -328,7 +336,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
child: Card( child: Card(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
context.push('/auth/create'); context.push('/auth/create-account');
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -367,12 +375,23 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
), ),
), ),
const Gap(8), const Gap(8),
TextButton( Row(
onPressed: () { mainAxisAlignment: MainAxisAlignment.center,
context.push('/settings'); children: [
}, TextButton(
child: Text('appSettings').tr(), onPressed: () {
).center(), context.push('/about');
},
child: Text('about').tr(),
),
TextButton(
onPressed: () {
context.push('/settings');
},
child: Text('appSettings').tr(),
),
],
),
], ],
), ),
).center(), ).center(),

View File

@ -82,7 +82,7 @@ class EventCalanderScreen extends HookConsumerWidget {
), ),
// Show user profile if viewing someone else's calendar // Show user profile if viewing someone else's calendar
if (name != 'me' && user.hasValue) if (name != 'me' && user.value != null)
AccountNameplate(name: name), AccountNameplate(name: name),
], ],
), ),
@ -106,7 +106,7 @@ class EventCalanderScreen extends HookConsumerWidget {
).padding(horizontal: 8, vertical: 4), ).padding(horizontal: 8, vertical: 4),
// Show user profile if viewing someone else's calendar // Show user profile if viewing someone else's calendar
if (name != 'me' && user.hasValue) if (name != 'me' && user.value != null)
AccountNameplate(name: name), AccountNameplate(name: name),
Gap(MediaQuery.of(context).padding.bottom + 16), Gap(MediaQuery.of(context).padding.bottom + 16),
], ],

View File

@ -22,6 +22,7 @@ import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/safety/abuse_report_helper.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:palette_generator/palette_generator.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -72,6 +73,8 @@ Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
@riverpod @riverpod
Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async { Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async {
final userInfo = ref.watch(userInfoProvider);
if (userInfo.value == null) return null;
final account = await ref.watch(accountProvider(uname).future); final account = await ref.watch(accountProvider(uname).future);
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);
try { try {
@ -87,6 +90,8 @@ Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async {
@riverpod @riverpod
Future<SnRelationship?> accountRelationship(Ref ref, String uname) async { Future<SnRelationship?> accountRelationship(Ref ref, String uname) async {
final userInfo = ref.watch(userInfoProvider);
if (userInfo.value == null) return null;
final account = await ref.watch(accountProvider(uname).future); final account = await ref.watch(accountProvider(uname).future);
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);
try { try {
@ -139,6 +144,23 @@ class AccountProfileScreen extends HookConsumerWidget {
} }
} }
Future<void> blockAction() async {
showLoadingModal(context);
try {
final client = ref.watch(apiClientProvider);
if (accountRelationship.value == null) {
await client.post('/relationships/${account.value!.id}/block');
} else {
await client.delete('/relationships/${account.value!.id}/block');
}
ref.invalidate(accountRelationshipProvider(name));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> directMessageAction() async { Future<void> directMessageAction() async {
if (!account.hasValue) return; if (!account.hasValue) return;
if (accountChat.value != null) { if (accountChat.value != null) {
@ -219,6 +241,8 @@ class AccountProfileScreen extends HookConsumerWidget {
]; ];
} }
final user = ref.watch(userInfoProvider);
return account.when( return account.when(
data: data:
(data) => AppScaffold( (data) => AppScaffold(
@ -379,40 +403,86 @@ class AccountProfileScreen extends HookConsumerWidget {
).padding(horizontal: 24), ).padding(horizontal: 24),
), ),
SliverToBoxAdapter( if (user.value != null)
child: const Divider(height: 1).padding(top: 24, bottom: 12), SliverToBoxAdapter(
), child: const Divider(
height: 1,
).padding(top: 24, bottom: 12),
),
if (user.value != null)
SliverToBoxAdapter(
child: Row(
spacing: 8,
children: [
if (accountRelationship.value == null ||
accountRelationship.value!.status > -100)
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.secondary,
),
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(
context,
).colorScheme.onSecondary,
),
),
onPressed: relationshipAction,
label:
Text(
accountRelationship.value == null
? 'addFriendShort'
: 'added',
).tr(),
icon:
accountRelationship.value == null
? const Icon(Symbols.person_add)
: const Icon(Symbols.person_check),
),
),
if (accountRelationship.value == null ||
accountRelationship.value!.status <= -100)
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.secondary,
),
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(
context,
).colorScheme.onSecondary,
),
),
onPressed: blockAction,
label:
Text(
accountRelationship.value == null
? 'blockUser'
: 'unblockUser',
).tr(),
icon:
accountRelationship.value == null
? const Icon(Symbols.block)
: const Icon(Symbols.person_cancel),
),
),
],
).padding(horizontal: 16),
),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Row( child: Row(
spacing: 8, spacing: 8,
children: [ children: [
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.secondary,
),
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.onSecondary,
),
),
onPressed: relationshipAction,
label:
Text(
accountRelationship.value == null
? 'addFriendShort'
: 'added',
).tr(),
icon:
accountRelationship.value == null
? const Icon(Symbols.person_add)
: const Icon(Symbols.person_check),
),
),
Expanded( Expanded(
child: FilledButton.icon( child: FilledButton.icon(
onPressed: directMessageAction, onPressed: directMessageAction,
@ -426,8 +496,25 @@ class AccountProfileScreen extends HookConsumerWidget {
).tr(), ).tr(),
), ),
), ),
IconButton.filled(
onPressed: () {
showAbuseReportSheet(
context,
resourceIdentifier: 'account/${data.id}',
);
},
icon: Icon(
Symbols.flag,
color: Theme.of(context).colorScheme.onError,
),
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
Theme.of(context).colorScheme.error,
),
),
),
], ],
).padding(horizontal: 16), ).padding(horizontal: 16, top: 4),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: const Divider(height: 1).padding(top: 12), child: const Divider(height: 1).padding(top: 12),

View File

@ -395,7 +395,7 @@ class _AccountAppbarForcegroundColorProviderElement
String get uname => (origin as AccountAppbarForcegroundColorProvider).uname; String get uname => (origin as AccountAppbarForcegroundColorProvider).uname;
} }
String _$accountDirectChatHash() => r'60d0015fc2a3c8fc2190bb41d6818cf3027d9d0a'; String _$accountDirectChatHash() => r'3d28c8ba8079159f724fe3cd47bbe00db55cedcc';
/// See also [accountDirectChat]. /// See also [accountDirectChat].
@ProviderFor(accountDirectChat) @ProviderFor(accountDirectChat)
@ -517,7 +517,7 @@ class _AccountDirectChatProviderElement
} }
String _$accountRelationshipHash() => String _$accountRelationshipHash() =>
r'cb7d0d3f8cd4f23ad9d2d529872c540dac483d4f'; r'0be2420e1f6a65b8dcead9617191471924aaf232';
/// See also [accountRelationship]. /// See also [accountRelationship].
@ProviderFor(accountRelationship) @ProviderFor(accountRelationship)

View File

@ -1,105 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/article_detail.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/loading_indicator.dart';
import 'package:html2md/html2md.dart' as html2md;
class ArticleDetailScreen extends ConsumerWidget {
final String articleId;
const ArticleDetailScreen({super.key, required this.articleId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final articleAsync = ref.watch(articleDetailProvider(articleId));
return AppScaffold(
body: articleAsync.when(
data:
(article) => AppScaffold(
appBar: AppBar(
leading: const BackButton(),
title: Text(article.title),
),
body: _ArticleDetailContent(article: article),
),
loading: () => const Center(child: LoadingIndicator()),
error:
(error, stackTrace) =>
Center(child: Text('Failed to load article: $error')),
),
);
}
}
class _ArticleDetailContent extends HookConsumerWidget {
final SnWebArticle article;
const _ArticleDetailContent({required this.article});
@override
Widget build(BuildContext context, WidgetRef ref) {
final markdownContent = useMemoized(
() => html2md.convert(article.content ?? ''),
[article],
);
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (article.preview?.imageUrl != null)
Image.network(
article.preview!.imageUrl!,
width: double.infinity,
height: 200,
fit: BoxFit.cover,
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
article.title,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
if (article.feed?.title != null)
Text(
article.feed!.title,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const Divider(height: 32),
if (article.content != null)
...MarkdownTextContent.buildGenerator(
isDark: Theme.of(context).brightness == Brightness.dark,
).buildWidgets(markdownContent)
else if (article.preview?.description != null)
Text(article.preview!.description!),
const Gap(24),
FilledButton(
onPressed:
() => launchUrlString(
article.url,
mode: LaunchMode.externalApplication,
),
child: const Text('Read Full Article'),
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
),
],
),
);
}
}

View File

@ -28,7 +28,7 @@ class StickersScreen extends HookConsumerWidget {
actions: [ actions: [
IconButton( IconButton(
onPressed: () { onPressed: () {
context.push('/creators/stickers/new?pubName=pubName').then(( context.push('/creators/stickers/new?pubName=$pubName').then((
value, value,
) { ) {
if (value != null) { if (value != null) {

View File

@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/webfeed.dart'; import 'package:island/models/webfeed.dart';
import 'package:island/pods/webfeed.dart'; import 'package:island/pods/webfeed.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@ -183,7 +184,7 @@ class WebFeedEditScreen extends HookConsumerWidget {
} }
}, [pubName, feedId, ref, context]); }, [pubName, feedId, ref, context]);
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: Text(hasFeedId ? 'Edit Web Feed' : 'New Web Feed'), title: Text(hasFeedId ? 'Edit Web Feed' : 'New Web Feed'),
actions: [ actions: [

View File

@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/article_detail.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/loading_indicator.dart';
import 'package:html2md/html2md.dart' as html2md;
class ArticleDetailScreen extends ConsumerWidget {
final String articleId;
const ArticleDetailScreen({super.key, required this.articleId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final articleAsync = ref.watch(articleDetailProvider(articleId));
return AppScaffold(
body: articleAsync.when(
data:
(article) => AppScaffold(
appBar: AppBar(
leading: const BackButton(),
title: Text(article.title),
),
body: _ArticleDetailContent(article: article),
),
loading: () => const Center(child: LoadingIndicator()),
error:
(error, stackTrace) =>
Center(child: Text('Failed to load article: $error')),
),
);
}
}
class _ArticleDetailContent extends HookConsumerWidget {
final SnWebArticle article;
const _ArticleDetailContent({required this.article});
@override
Widget build(BuildContext context, WidgetRef ref) {
final markdownContent = useMemoized(
() => html2md.convert(article.content ?? ''),
[article],
);
return SingleChildScrollView(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (article.preview?.imageUrl != null)
Image.network(
article.preview!.imageUrl!,
width: double.infinity,
height: 200,
fit: BoxFit.cover,
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
article.title,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
if (article.feed?.title != null)
Text(
article.feed!.title,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const Divider(height: 32),
if (article.content != null)
...MarkdownTextContent.buildGenerator(
isDark: Theme.of(context).brightness == Brightness.dark,
).buildWidgets(markdownContent)
else if (article.preview?.description != null)
Text(article.preview!.description!),
const Gap(24),
FilledButton(
onPressed:
() => launchUrlString(
article.url,
mode: LaunchMode.externalApplication,
),
child: const Text('Read Full Article'),
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
),
],
),
),
),
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/webfeed.dart'; import 'package:island/models/webfeed.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/web_article_card.dart'; import 'package:island/widgets/web_article_card.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';
@ -124,18 +125,23 @@ class ArticlesScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Scaffold( return AppScaffold(
appBar: AppBar(title: Text(title ?? 'Articles')), appBar: AppBar(title: Text(title ?? 'Articles')),
body: CustomScrollView( body: Center(
slivers: [ child: ConstrainedBox(
SliverPadding( constraints: const BoxConstraints(maxWidth: 560),
padding: const EdgeInsets.only(top: 8, left: 8, right: 8), child: CustomScrollView(
sliver: SliverArticlesList( slivers: [
feedId: feedId, SliverPadding(
publisherId: publisherId, padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
), sliver: SliverArticlesList(
feedId: feedId,
publisherId: publisherId,
),
),
],
), ),
], ),
), ),
); );
} }

View File

@ -307,7 +307,7 @@ class _ActivityListView extends HookConsumerWidget {
return CustomScrollView( return CustomScrollView(
slivers: [ slivers: [
if (user.hasValue && !contentOnly) if (user.value != null && !contentOnly)
SliverToBoxAdapter(child: CheckInWidget()), SliverToBoxAdapter(child: CheckInWidget()),
SliverList.builder( SliverList.builder(
itemCount: widgetCount, itemCount: widgetCount,
@ -338,7 +338,7 @@ class _ActivityListView extends HookConsumerWidget {
bottom: 16, bottom: 16,
) )
: null, : null,
onRefresh: (_) { onRefresh: () {
activitiesNotifier.forceRefresh(); activitiesNotifier.forceRefresh();
}, },
onUpdate: (post) { onUpdate: (post) {

View File

@ -321,8 +321,15 @@ class ArticleComposeScreen extends HookConsumerWidget {
builder: (context, attachments, _) { builder: (context, attachments, _) {
if (attachments.isEmpty) return const SizedBox.shrink(); if (attachments.isEmpty) return const SizedBox.shrink();
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Gap(16), const Gap(16),
Text(
'articleAttachmentHint'.tr(),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
).padding(bottom: 8),
ValueListenableBuilder<Map<int, double>>( ValueListenableBuilder<Map<int, double>>(
valueListenable: state.attachmentProgress, valueListenable: state.attachmentProgress,
builder: (context, progressMap, _) { builder: (context, progressMap, _) {
@ -332,8 +339,8 @@ class ArticleComposeScreen extends HookConsumerWidget {
children: [ children: [
for (var idx = 0; idx < attachments.length; idx++) for (var idx = 0; idx < attachments.length; idx++)
SizedBox( SizedBox(
width: 120, width: 280,
height: 120, height: 280,
child: AttachmentPreview( child: AttachmentPreview(
item: attachments[idx], item: attachments[idx],
progress: progressMap[idx], progress: progressMap[idx],
@ -358,6 +365,12 @@ class ArticleComposeScreen extends HookConsumerWidget {
delta, delta,
); );
}, },
onInsert:
() => ComposeLogic.insertAttachment(
ref,
state,
idx,
),
), ),
), ),
], ],

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/post/post_item.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
@ -107,7 +108,7 @@ class _PostSearchScreenState extends ConsumerState<PostSearchScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: TextField( title: TextField(
controller: _searchController, controller: _searchController,

View File

@ -187,7 +187,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
), ),
onTap: () { onTap: () {
Navigator.pop(context, true); Navigator.pop(context, true);
context.push('/account/${data.name}'); context.push('/account/${data.account?.name}');
}, },
), ),
Expanded( Expanded(

View File

@ -77,6 +77,7 @@ class RealmDetailScreen extends HookConsumerWidget {
); );
return AppScaffold( return AppScaffold(
noBackground: false,
body: realmState.when( body: realmState.when(
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')), error: (error, _) => Center(child: Text('Error: $error')),

View File

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/abuse_report.dart';
import 'package:island/models/abuse_report_type.dart';
import 'package:island/services/abuse_report_service.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:styled_widget/styled_widget.dart';
class AbuseReportDetailScreen extends ConsumerStatefulWidget {
final String reportId;
const AbuseReportDetailScreen({super.key, required this.reportId});
@override
ConsumerState<AbuseReportDetailScreen> createState() =>
_AbuseReportDetailScreenState();
}
class _AbuseReportDetailScreenState
extends ConsumerState<AbuseReportDetailScreen> {
Future<SnAbuseReport>? _reportFuture;
@override
void initState() {
super.initState();
_reportFuture = ref
.read(abuseReportServiceProvider)
.getReport(widget.reportId);
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(title: const Text('Abuse Report Details')),
body: FutureBuilder<SnAbuseReport>(
future: _reportFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
final report = snapshot.data!;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow(context, 'Report ID', report.id),
_buildDetailRow(
context,
'Resource Identifier',
report.resourceIdentifier,
),
_buildDetailRow(
context,
'Type',
AbuseReportType.fromValue(report.type).displayName,
),
_buildDetailRow(context, 'Reason', report.reason),
_buildDetailRow(
context,
'Resolved At',
report.resolvedAt?.toString() ?? 'N/A',
),
_buildDetailRow(
context,
'Resolution',
report.resolution ?? 'N/A',
),
_buildDetailRow(context, 'Account ID', report.accountId),
_buildDetailRow(
context,
'Created At',
report.createdAt.toString(),
),
_buildDetailRow(
context,
'Updated At',
report.updatedAt.toString(),
),
],
),
);
} else {
return const Center(child: Text('No data'));
}
},
),
);
}
Widget _buildDetailRow(BuildContext context, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: Theme.of(context).textTheme.titleMedium).bold(),
Text(value, style: Theme.of(context).textTheme.bodyLarge),
],
),
);
}
}

View File

@ -0,0 +1,153 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/abuse_report.dart';
import 'package:island/models/abuse_report_type.dart';
import 'package:island/services/abuse_report_service.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/safety/abuse_report_helper.dart';
class AbuseReportListScreen extends ConsumerStatefulWidget {
const AbuseReportListScreen({super.key});
@override
ConsumerState<AbuseReportListScreen> createState() =>
_AbuseReportListScreenState();
}
class _AbuseReportListScreenState extends ConsumerState<AbuseReportListScreen> {
Future<List<SnAbuseReport>>? _reportsFuture;
@override
void initState() {
super.initState();
_reportsFuture = ref.read(abuseReportServiceProvider).getReports();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(title: Text('abuseReports').tr()),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
showAbuseReportSheet(context, resourceIdentifier: 'unidentified');
},
),
body: FutureBuilder<List<SnAbuseReport>>(
future: _reportsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
final reports = snapshot.data!;
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: reports.length,
itemBuilder: (context, index) {
final report = reports[index];
return Card(
elevation: 2,
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: InkWell(
onTap: () {
context.push('/safety/reports/me/${report.id}');
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
report.reason,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'ID',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
report.id,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Type',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
AbuseReportType.fromValue(
report.type,
).displayName,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Created at',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
'${report.createdAt.formatRelative(context)} · ${report.createdAt.formatSystem()}',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Status',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
report.resolvedAt != null
? 'Resolved'
: 'Unresolved',
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(
color:
report.resolvedAt != null
? Colors.green
: Colors.orange,
),
),
],
),
],
),
),
),
);
},
);
} else {
return const Center(child: Text('No data'));
}
},
),
);
}
}

View File

@ -0,0 +1,25 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/abuse_report.dart';
import 'package:island/pods/network.dart';
final abuseReportServiceProvider = Provider<AbuseReportService>((ref) {
return AbuseReportService(ref);
});
class AbuseReportService {
final Ref ref;
AbuseReportService(this.ref);
Future<SnAbuseReport> getReport(String id) async {
final response =
await ref.read(apiClientProvider).get('/safety/reports/me/$id');
return SnAbuseReport.fromJson(response.data);
}
Future<List<SnAbuseReport>> getReports() async {
final response = await ref.read(apiClientProvider).get('/safety/reports/me');
return (response.data as List)
.map((json) => SnAbuseReport.fromJson(json))
.toList();
}
}

View File

@ -63,9 +63,11 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
}); });
} }
Future<void> subscribePushNotification(Dio apiClient) async { Future<void> subscribePushNotification(
Dio apiClient, {
bool detailedErrors = false,
}) async {
await FirebaseMessaging.instance.requestPermission( await FirebaseMessaging.instance.requestPermission(
provisional: true,
alert: true, alert: true,
badge: true, badge: true,
sound: true, sound: true,
@ -97,6 +99,8 @@ Future<void> subscribePushNotification(Dio apiClient) async {
deviceToken, deviceToken,
!kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1, !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
); );
} else if (detailedErrors) {
throw Exception("Failed to get device token for push notifications.");
} }
} }

View File

@ -3,7 +3,6 @@ import 'dart:convert';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/widgets/tour/techincal_review_intro.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tour.g.dart'; part 'tour.g.dart';
@ -12,7 +11,7 @@ part 'tour.freezed.dart';
const kAppTourStatusKey = "app_tour_statuses"; const kAppTourStatusKey = "app_tour_statuses";
const List<Tour> kAllTours = [ const List<Tour> kAllTours = [
Tour(id: 'technical_review_intro', isStartup: true), // Tour(id: 'technical_review_intro', isStartup: true),
]; ];
@freezed @freezed
@ -22,7 +21,7 @@ sealed class Tour with _$Tour {
const factory Tour({required String id, required bool isStartup}) = _Tour; const factory Tour({required String id, required bool isStartup}) = _Tour;
Widget get widget => switch (id) { Widget get widget => switch (id) {
'technical_review_intro' => const TechicalReviewIntroWidget(), // 'technical_review_intro' => const TechicalReviewIntroWidget(),
_ => throw UnimplementedError(), _ => throw UnimplementedError(),
}; };
} }

View File

@ -0,0 +1,22 @@
String getAbuseReportTypeString(int type) {
switch (type) {
case 0:
return 'Copyright';
case 1:
return 'Harassment';
case 2:
return 'Impersonation';
case 3:
return 'Offensive Content';
case 4:
return 'Spam';
case 5:
return 'Privacy Violation';
case 6:
return 'Illegal Content';
case 7:
return 'Other';
default:
return 'Unknown';
}
}

View File

@ -59,7 +59,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget {
}, },
options: Options(method: initialStatus == null ? 'POST' : 'PATCH'), options: Options(method: initialStatus == null ? 'POST' : 'PATCH'),
); );
if (user.hasValue) { if (user.value != null) {
ref.invalidate(accountStatusProvider(user.value!.name)); ref.invalidate(accountStatusProvider(user.value!.name));
} }
if (!context.mounted) return; if (!context.mounted) return;

View File

@ -350,7 +350,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
return AnimatedPositioned( return AnimatedPositioned(
duration: Duration(milliseconds: 1850), duration: Duration(milliseconds: 1850),
top: top:
!user.hasValue || user.value == null ||
user.value == null || user.value == null ||
websocketState == WebSocketState.connected() websocketState == WebSocketState.connected()
? -indicatorHeight ? -indicatorHeight
@ -362,7 +362,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
child: IgnorePointer( child: IgnorePointer(
child: Material( child: Material(
elevation: elevation:
!user.hasValue || websocketState == WebSocketState.connected() user.value == null || websocketState == WebSocketState.connected()
? 0 ? 0
: 4, : 4,
child: AnimatedContainer( child: AnimatedContainer(

View File

@ -44,7 +44,7 @@ class AudioCallButton extends HookConsumerWidget {
try { try {
await apiClient.post('/chat/realtime/$roomId'); await apiClient.post('/chat/realtime/$roomId');
if (context.mounted) { if (context.mounted) {
context.push('/chat/call/$roomId'); context.push('/chat/$roomId/call');
} }
} catch (e) { } catch (e) {
showErrorAlert(e); showErrorAlert(e);

View File

@ -360,7 +360,7 @@ class CallOverlayBar extends HookConsumerWidget {
).padding(all: 16), ).padding(all: 16),
), ),
onTap: () { onTap: () {
context.push('/chat/call/${callNotifier.roomId!}'); context.push('/chat/${callNotifier.roomId!}/call');
}, },
); );
} }

View File

@ -15,6 +15,7 @@ class AttachmentPreview extends StatelessWidget {
final double? progress; final double? progress;
final Function(int)? onMove; final Function(int)? onMove;
final Function? onDelete; final Function? onDelete;
final Function? onInsert;
final Function? onRequestUpload; final Function? onRequestUpload;
const AttachmentPreview({ const AttachmentPreview({
super.key, super.key,
@ -23,13 +24,17 @@ class AttachmentPreview extends StatelessWidget {
this.onRequestUpload, this.onRequestUpload,
this.onMove, this.onMove,
this.onDelete, this.onDelete,
this.onInsert,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var ratio =
(item.isOnCloud ? (item.data.fileMeta?['ratio'] ?? 1) : 1).toDouble();
if (ratio == 0) ratio = 1.0;
return AspectRatio( return AspectRatio(
aspectRatio: aspectRatio: ratio,
(item.isOnCloud ? (item.data.fileMeta?['ratio'] ?? 1) : 1).toDouble(),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Stack( child: Stack(
@ -104,7 +109,11 @@ class AttachmentPreview extends StatelessWidget {
style: TextStyle(color: Colors.white), style: TextStyle(color: Colors.white),
), ),
Gap(6), Gap(6),
Center(child: LinearProgressIndicator(value: progress)), Center(
child: LinearProgressIndicator(
value: progress != null ? progress! / 100.0 : null,
),
),
], ],
), ),
), ),
@ -166,6 +175,18 @@ class AttachmentPreview extends StatelessWidget {
onMove?.call(1); onMove?.call(1);
}, },
), ),
if (onInsert != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.add,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onInsert?.call();
},
),
], ],
), ),
), ),

View File

@ -37,13 +37,10 @@ class CloudFileList extends HookConsumerWidget {
double calculateAspectRatio() { double calculateAspectRatio() {
double total = 0; double total = 0;
for (var ratio in files.map( for (var ratio in files.map((e) => e.fileMeta?['ratio'] ?? 1)) {
(e) =>
e.fileMeta?['ratio'] ??
((e.mimeType?.startsWith('image') ?? false) ? 1 : 16 / 9),
)) {
total += ratio; total += ratio;
} }
if (total == 0) return 1;
return total / files.length; return total / files.length;
} }

View File

@ -25,22 +25,21 @@ class CloudFileWidget extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final serverUrl = ref.watch(serverUrlProvider); final serverUrl = ref.watch(serverUrlProvider);
final uri = '$serverUrl/files/${item.id}'; final uri = '$serverUrl/api/files/${item.id}';
var ratio = (item.fileMeta?['ratio'] ?? 1).toDouble();
if (ratio == 0) ratio = 1.0;
final content = switch (item.mimeType?.split('/').firstOrNull) { final content = switch (item.mimeType?.split('/').firstOrNull) {
"image" => AspectRatio( "image" => AspectRatio(
aspectRatio: (item.fileMeta?['ratio'] ?? 1).toDouble(), aspectRatio: ratio,
child: UniversalImage( child: UniversalImage(
uri: uri, uri: uri,
blurHash: noBlurhash ? null : item.fileMeta?['blur'], blurHash: noBlurhash ? null : item.fileMeta?['blur'],
), ),
), ),
"video" => AspectRatio( "video" => AspectRatio(
aspectRatio: (item.fileMeta?['ratio'] ?? 16 / 9).toDouble(), aspectRatio: ratio,
child: UniversalVideo( child: UniversalVideo(uri: uri, aspectRatio: ratio),
uri: uri,
aspectRatio: (item.fileMeta?['ratio'] ?? 16 / 9).toDouble(),
),
), ),
_ => Text('Unable render for ${item.mimeType}'), _ => Text('Unable render for ${item.mimeType}'),
}; };
@ -71,7 +70,7 @@ class CloudImageWidget extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final serverUrl = ref.watch(serverUrlProvider); final serverUrl = ref.watch(serverUrlProvider);
final uri = '$serverUrl/files/${file?.id ?? fileId}'; final uri = '$serverUrl/api/files/${file?.id ?? fileId}';
return AspectRatio( return AspectRatio(
aspectRatio: aspectRatio, aspectRatio: aspectRatio,
@ -87,7 +86,7 @@ class CloudImageWidget extends ConsumerWidget {
required String serverUrl, required String serverUrl,
bool original = false, bool original = false,
}) { }) {
final uri = '$serverUrl/files/$fileId?original=$original'; final uri = '$serverUrl/api/files/$fileId?original=$original';
return CachedNetworkImageProvider(uri); return CachedNetworkImageProvider(uri);
} }
} }
@ -110,7 +109,7 @@ class ProfilePictureWidget extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final serverUrl = ref.watch(serverUrlProvider); final serverUrl = ref.watch(serverUrlProvider);
final uri = '$serverUrl/files/${file?.id ?? fileId}'; final uri = '$serverUrl/api/files/${file?.id ?? fileId}';
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(radius)), borderRadius: BorderRadius.all(Radius.circular(radius)),
@ -303,7 +302,7 @@ class SplitAvatarWidget extends ConsumerWidget {
} }
final serverUrl = ref.watch(serverUrlProvider); final serverUrl = ref.watch(serverUrlProvider);
final uri = '$serverUrl/files/$fileId'; final uri = '$serverUrl/api/files/$fileId';
return SizedBox( return SizedBox(
width: radius, width: radius,

View File

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
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';
@ -6,11 +7,14 @@ import 'package:flutter_highlight/themes/a11y-dark.dart';
import 'package:flutter_highlight/themes/a11y-light.dart'; import 'package:flutter_highlight/themes/a11y-light.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:island/models/file.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/markdown_latex.dart'; import 'package:island/widgets/content/markdown_latex.dart';
import 'package:markdown/markdown.dart' as markdown; import 'package:markdown/markdown.dart' as markdown;
import 'package:markdown_widget/markdown_widget.dart'; import 'package:markdown_widget/markdown_widget.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'image.dart'; import 'image.dart';
@ -23,6 +27,7 @@ class MarkdownTextContent extends HookConsumerWidget {
final TextStyle? linkStyle; final TextStyle? linkStyle;
final EdgeInsets? linesMargin; final EdgeInsets? linesMargin;
final bool isSelectable; final bool isSelectable;
final List<SnCloudFile>? attachments;
const MarkdownTextContent({ const MarkdownTextContent({
super.key, super.key,
@ -33,6 +38,7 @@ class MarkdownTextContent extends HookConsumerWidget {
this.linkStyle, this.linkStyle,
this.isSelectable = false, this.isSelectable = false,
this.linesMargin, this.linesMargin,
this.attachments,
}); });
@override @override
@ -109,6 +115,29 @@ class MarkdownTextContent extends HookConsumerWidget {
final uri = Uri.parse(url); final uri = Uri.parse(url);
if (uri.scheme == 'solian') { if (uri.scheme == 'solian') {
switch (uri.host) { switch (uri.host) {
case 'files':
final file = attachments?.firstWhereOrNull(
(file) => file.id == uri.pathSegments[0],
);
if (file == null) {
return const SizedBox.shrink();
}
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: CloudFileWidget(
item: file,
fit: BoxFit.cover,
).clipRRect(all: 8),
),
);
case 'stickers': case 'stickers':
final size = doesEnlargeSticker ? 96.0 : 24.0; final size = doesEnlargeSticker ? 96.0 : 24.0;
return ClipRRect( return ClipRRect(
@ -132,9 +161,9 @@ class MarkdownTextContent extends HookConsumerWidget {
); );
} }
} }
final content = UniversalImage( final content = ConstrainedBox(
uri: uri.toString(), constraints: BoxConstraints(maxHeight: 360),
fit: BoxFit.cover, child: UniversalImage(uri: uri.toString(), fit: BoxFit.contain),
); );
return content; return content;
}, },

View File

@ -70,7 +70,7 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
return Video( return Video(
controller: _videoController!, controller: _videoController!,
aspectRatio: widget.aspectRatio, aspectRatio: widget.aspectRatio != 1 ? widget.aspectRatio : null,
controls: controls:
!kIsWeb && (Platform.isAndroid || Platform.isIOS) !kIsWeb && (Platform.isAndroid || Platform.isIOS)
? MaterialVideoControls ? MaterialVideoControls

View File

@ -474,6 +474,23 @@ class ComposeLogic {
state.attachments.value = clone; state.attachments.value = clone;
} }
static void insertAttachment(WidgetRef ref, ComposeState state, int index) {
final attachment = state.attachments.value[index];
if (!attachment.isOnCloud) {
return;
}
final cloudFile = attachment.data as SnCloudFile;
final markdown = '![${cloudFile.name}](solian://files/${cloudFile.id})';
final controller = state.contentController;
final text = controller.text;
final selection = controller.selection;
final newText = text.replaceRange(selection.start, selection.end, markdown);
controller.text = newText;
controller.selection = TextSelection.fromPosition(
TextPosition(offset: selection.start + markdown.length),
);
}
static Future<void> performAction( static Future<void> performAction(
WidgetRef ref, WidgetRef ref,
ComposeState state, ComposeState state,

View File

@ -56,7 +56,7 @@ class PostItem extends HookConsumerWidget {
final user = ref.watch(userInfoProvider); final user = ref.watch(userInfoProvider);
final isAuthor = useMemoized( final isAuthor = useMemoized(
() => user.hasValue && user.value?.id == item.publisher.accountId, () => user.value != null && user.value?.id == item.publisher.accountId,
[user], [user],
); );
@ -163,7 +163,7 @@ class PostItem extends HookConsumerWidget {
if ((item.repliedPost != null || item.forwardedPost != null) && if ((item.repliedPost != null || item.forwardedPost != null) &&
showReferencePost) showReferencePost)
_buildReferencePost(context, item), _buildReferencePost(context, item),
if (item.attachments.isNotEmpty) if (item.attachments.isNotEmpty && item.type != 1)
CloudFileList( CloudFileList(
files: item.attachments, files: item.attachments,
maxWidth: math.min( maxWidth: math.min(
@ -331,6 +331,7 @@ class PostItem extends HookConsumerWidget {
item.type == 0 item.type == 0
? EdgeInsets.only(bottom: 8) ? EdgeInsets.only(bottom: 8)
: null, : null,
attachments: item.attachments,
), ),
], ],
// Render tags and categories if they exist // Render tags and categories if they exist
@ -383,13 +384,18 @@ class PostItem extends HookConsumerWidget {
// Show truncation hint if post is truncated // Show truncation hint if post is truncated
if (item.isTruncated && !isFullPost && item.type != 1) if (item.isTruncated && !isFullPost && item.type != 1)
_PostTruncateHint().padding( _PostTruncateHint().padding(
bottom: item.attachments.isNotEmpty ? 8 : null, bottom:
(item.attachments.isNotEmpty ||
item.repliedPost != null ||
item.forwardedPost != null)
? 8
: null,
), ),
if ((item.repliedPost != null || if ((item.repliedPost != null ||
item.forwardedPost != null) && item.forwardedPost != null) &&
showReferencePost) showReferencePost)
_buildReferencePost(context, item), _buildReferencePost(context, item),
if (item.attachments.isNotEmpty) if (item.attachments.isNotEmpty && item.type != 1)
CloudFileList( CloudFileList(
files: item.attachments, files: item.attachments,
maxWidth: math.min( maxWidth: math.min(
@ -570,7 +576,7 @@ class PostItem extends HookConsumerWidget {
callback: () { callback: () {
showAbuseReportSheet( showAbuseReportSheet(
context, context,
resourceIdentifier: 'posts:${item.id}', resourceIdentifier: 'post/${item.id}',
); );
}, },
), ),
@ -689,6 +695,7 @@ Widget _buildReferencePost(BuildContext context, SnPost item) {
referencePost.type == 0 referencePost.type == 0
? EdgeInsets.only(bottom: 4) ? EdgeInsets.only(bottom: 4)
: null, : null,
attachments: item.attachments,
).padding(bottom: 4), ).padding(bottom: 4),
// Truncation hint for referenced post // Truncation hint for referenced post
if (referencePost.isTruncated) if (referencePost.isTruncated)
@ -696,7 +703,8 @@ Widget _buildReferencePost(BuildContext context, SnPost item) {
isCompact: true, isCompact: true,
margin: const EdgeInsets.only(top: 4, bottom: 8), margin: const EdgeInsets.only(top: 4, bottom: 8),
), ),
if (referencePost.attachments.isNotEmpty) if (referencePost.attachments.isNotEmpty &&
referencePost.type != 1)
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -1030,6 +1038,7 @@ class _ArticlePostDisplay extends StatelessWidget {
MarkdownTextContent( MarkdownTextContent(
content: item.content!, content: item.content!,
textStyle: Theme.of(context).textTheme.bodyLarge, textStyle: Theme.of(context).textTheme.bodyLarge,
attachments: item.attachments,
), ),
], ],
); );

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/post/post_replies.dart'; import 'package:island/widgets/post/post_replies.dart';
import 'package:island/widgets/post/post_quick_reply.dart'; import 'package:island/widgets/post/post_quick_reply.dart';
@ -14,6 +15,8 @@ class PostRepliesSheet extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userInfoProvider);
return SheetScaffold( return SheetScaffold(
titleText: 'repliesCount'.plural(post.repliesCount), titleText: 'repliesCount'.plural(post.repliesCount),
child: Column( child: Column(
@ -21,26 +24,29 @@ class PostRepliesSheet extends HookConsumerWidget {
// Replies list // Replies list
Expanded( Expanded(
child: CustomScrollView( child: CustomScrollView(
slivers: [PostRepliesList( slivers: [
postId: post.id.toString(), PostRepliesList(
backgroundColor: Colors.transparent, postId: post.id.toString(),
)], backgroundColor: Colors.transparent,
),
],
), ),
), ),
// Quick reply section // Quick reply section
Material( if (user.value != null)
elevation: 2, Material(
child: PostQuickReply( elevation: 2,
parent: post, child: PostQuickReply(
onPosted: () { parent: post,
ref.invalidate(postRepliesNotifierProvider(post.id)); onPosted: () {
}, ref.invalidate(postRepliesNotifierProvider(post.id));
).padding( },
bottom: MediaQuery.of(context).padding.bottom + 16, ).padding(
top: 16, bottom: MediaQuery.of(context).padding.bottom + 16,
horizontal: 16, top: 16,
horizontal: 16,
),
), ),
),
], ],
), ),
); );

View File

@ -55,7 +55,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin"))
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))

View File

@ -16,10 +16,10 @@ PODS:
- Firebase/Messaging (11.15.0): - Firebase/Messaging (11.15.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 11.15.0) - FirebaseMessaging (~> 11.15.0)
- firebase_core (3.15.0): - firebase_core (3.15.1):
- Firebase/CoreOnly (~> 11.15.0) - Firebase/CoreOnly (~> 11.15.0)
- FlutterMacOS - FlutterMacOS
- firebase_messaging (15.2.8): - firebase_messaging (15.2.9):
- Firebase/CoreOnly (~> 11.15.0) - Firebase/CoreOnly (~> 11.15.0)
- Firebase/Messaging (~> 11.15.0) - Firebase/Messaging (~> 11.15.0)
- firebase_core - firebase_core
@ -292,8 +292,8 @@ SPEC CHECKSUMS:
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_core: 177f51be1650b15d2d5b9f1abf48792619288070 firebase_core: 8dc569d17b3a9fc3ee5ebc21b322411b4a796833
firebase_messaging: 8748a5d4bb435993cffa7f5501292f3e914a23d7 firebase_messaging: adaf7fc22897a7aa49410d15f8a595bef2dbca2d
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843 FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
@ -310,7 +310,7 @@ SPEC CHECKSUMS:
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
livekit_client: c9d9f41996f5cf22b9ba0e8483e6af4ca5094059 livekit_client: c9d9f41996f5cf22b9ba0e8483e6af4ca5094059
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _flutterfire_internals name: _flutterfire_internals
sha256: "50e24b769bd1e725732f0aff18b806b8731c1fbcf4e8018ab98e7c4805a2a52f" sha256: a5788040810bd84400bc209913fbc40f388cded7cdf95ee2f5d2bff7e38d5241
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.57" version: "1.3.58"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
@ -349,10 +349,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: coverage name: coverage
sha256: aa07dbe5f2294c827b7edb9a87bba44a9c15a3cc81bc8da2ca19b37322d30080 sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.14.1" version: "1.15.0"
croppy: croppy:
dependency: "direct main" dependency: "direct main"
description: description:
@ -629,50 +629,50 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_core name: firebase_core
sha256: "5bba5924139e91d26446fd2601c18a6aa62c1161c768a989bb5e245dcdc20644" sha256: c6e8a6bf883d8ddd0dec39be90872daca65beaa6f4cff0051ed3b16c56b82e9f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.15.0" version: "3.15.1"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_core_platform_interface name: firebase_core_platform_interface
sha256: "5d2ab45779d91af2aa0252dec9fe4ee1caa015d83377de255454dcaa1526a0e0" sha256: "5dbc900677dcbe5873d22ad7fbd64b047750124f1f9b7ebe2a33b9ddccc838eb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.4.1" version: "6.0.0"
firebase_core_web: firebase_core_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_core_web name: firebase_core_web
sha256: eb3afccfc452b2b2075acbe0c4b27de62dd596802b4e5e19869c1e926cbb20b3 sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.24.0" version: "2.24.1"
firebase_messaging: firebase_messaging:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_messaging name: firebase_messaging
sha256: c6711cf2f455532b84a94022c7aaf85088849763af2f01b775ca79d82d10a01a sha256: "0f3363f97672eb9f65609fa00ed2f62cc8ec93e7e2d4def99726f9165d3d8a73"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.2.8" version: "15.2.9"
firebase_messaging_platform_interface: firebase_messaging_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_platform_interface name: firebase_messaging_platform_interface
sha256: "1c9dacccb1aee1bf17ba519dda5563a16fdd2ec1e79b5f2e421cb4bf75a166f7" sha256: "7a05ef119a14c5f6a9440d1e0223bcba20c8daf555450e119c4c477bf2c3baa9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.6.8" version: "4.6.9"
firebase_messaging_web: firebase_messaging_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_web name: firebase_messaging_web
sha256: "54317c26fa92f0d90a2017977ac791cb0504eca29fcf397f06adf727d4a7a2d5" sha256: a4547f76da2a905190f899eb4d0150e1d0fd52206fce469d9f05ae15bb68b2c5
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.10.8" version: "3.10.9"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -1049,18 +1049,18 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: freezed name: freezed
sha256: "6022db4c7bfa626841b2a10f34dd1e1b68e8f8f9650db6112dcdeeca45ca793c" sha256: "2d399f823b8849663744d2a9ddcce01c49268fb4170d0442a655bf6a2f47be22"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.6" version: "3.1.0"
freezed_annotation: freezed_annotation:
dependency: "direct main" dependency: "direct main"
description: description:
name: freezed_annotation name: freezed_annotation
sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.1.0"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -1385,10 +1385,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: local_auth_darwin name: local_auth_darwin
sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2" sha256: "25163ce60a5a6c468cf7a0e3dc8a165f824cabc2aa9e39a5e9fc5c2311b7686f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.3" version: "1.5.0"
local_auth_platform_interface: local_auth_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -2174,10 +2174,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_helper name: source_helper
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" sha256: "4f81479fe5194a622cdd1713fe1ecb683a6e6c85cd8cec8e2e35ee5ab3fdf2a1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.5" version: "1.3.6"
source_map_stack_trace: source_map_stack_trace:
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: 3.0.0+110 version: 3.1.0+113
environment: environment:
sdk: ^3.7.2 sdk: ^3.7.2

View File

@ -0,0 +1,25 @@
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": [
"W7HPZ53V6B.dev.solsynth.solian"
],
"paths": [
"*"
],
"components": [
{
"/": "/*"
}
]
}
]
},
"webcredentials": {
"apps": [
"W7HPZ53V6B.dev.solsynth.solian"
]
}
}

View File

@ -0,0 +1,12 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "dev.solsynth.solian",
"sha256_cert_fingerprints": [
"57:0C:A4:E6:1F:57:DF:56:70:42:05:4B:43:E2:DD:9E:00:E6:77:C3:D8:3C:5F:D5:A0:05:59:30:5A:85:F9:BC"
]
}
}
]