diff --git a/assets/icon-macos.png b/assets/icon-macos.png new file mode 100644 index 0000000..d7017f5 Binary files /dev/null and b/assets/icon-macos.png differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100755 index 0000000..e5c8d48 Binary files /dev/null and b/assets/icon.png differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100755 index 0000000..e414a9d Binary files /dev/null and b/assets/logo.png differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..9116946 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,35 @@ +PODS: + - Flutter (1.0.0) + - flutter_secure_storage (6.0.0): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + flutter_secure_storage: + :path: ".symlinks/plugins/flutter_secure_storage/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + +PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 + +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index da6be28..30118d7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,8 +8,10 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 235F0E9F565D7DEE558EA544 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 875B1905BB09FD3E418F83BE /* Pods_RunnerTests.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 697629BCCB242B335F9740F6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 644CB23863BAC87225224BEB /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -40,14 +42,20 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 02A460D36C4C66B1E6179D1B /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 644CB23863BAC87225224BEB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7507B5B1756DA08A398095AC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 875B1905BB09FD3E418F83BE /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8C092932B0B7297947BE9263 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 925B960477E1606F0EF59C87 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -55,13 +63,24 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DD2E0D3CBC50FE4BCEF3770A /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + F667C77545827013E3F0CF22 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 094D70693A1A66F8FE5AC566 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 235F0E9F565D7DEE558EA544 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 697629BCCB242B335F9740F6 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,6 +95,29 @@ path = RunnerTests; sourceTree = ""; }; + 47971B15D8567924545E35C5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 644CB23863BAC87225224BEB /* Pods_Runner.framework */, + 875B1905BB09FD3E418F83BE /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 7BA6BD8939A7BE19A2C7086C /* Pods */ = { + isa = PBXGroup; + children = ( + 7507B5B1756DA08A398095AC /* Pods-Runner.debug.xcconfig */, + 8C092932B0B7297947BE9263 /* Pods-Runner.release.xcconfig */, + F667C77545827013E3F0CF22 /* Pods-Runner.profile.xcconfig */, + 925B960477E1606F0EF59C87 /* Pods-RunnerTests.debug.xcconfig */, + DD2E0D3CBC50FE4BCEF3770A /* Pods-RunnerTests.release.xcconfig */, + 02A460D36C4C66B1E6179D1B /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -94,6 +136,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 7BA6BD8939A7BE19A2C7086C /* Pods */, + 47971B15D8567924545E35C5 /* Frameworks */, ); sourceTree = ""; }; @@ -128,8 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 259653AE41D478F4C6BAE9B2 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + 094D70693A1A66F8FE5AC566 /* Frameworks */, ); buildRules = ( ); @@ -145,12 +191,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + B1CDA9DD5B638A2BB88053CB /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 287A33C298CA352A7E7F32A4 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -222,6 +270,45 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 259653AE41D478F4C6BAE9B2 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 287A33C298CA352A7E7F32A4 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -253,6 +340,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + B1CDA9DD5B638A2BB88053CB /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -379,6 +488,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 925B960477E1606F0EF59C87 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -396,6 +506,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = DD2E0D3CBC50FE4BCEF3770A /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -411,6 +522,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 02A460D36C4C66B1E6179D1B /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/lib/models/account.dart b/lib/models/account.dart index e3321ee..cca8e1f 100644 --- a/lib/models/account.dart +++ b/lib/models/account.dart @@ -9,7 +9,6 @@ class Account { String banner; String description; String? emailAddress; - int powerLevel; int? externalId; Account({ @@ -23,7 +22,6 @@ class Account { required this.banner, required this.description, this.emailAddress, - required this.powerLevel, this.externalId, }); @@ -38,7 +36,6 @@ class Account { banner: json['banner'], description: json['description'], emailAddress: json['email_address'], - powerLevel: json['power_level'], externalId: json['external_id'], ); @@ -53,7 +50,6 @@ class Account { 'banner': banner, 'description': description, 'email_address': emailAddress, - 'power_level': powerLevel, 'external_id': externalId, }; } diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index 48a29da..1177b4d 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -5,14 +5,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get/get.dart'; import 'package:get/get_connect/http/src/request/request.dart'; -import 'package:solian/models/account.dart'; import 'package:solian/services.dart'; import 'package:oauth2/oauth2.dart' as oauth2; class AuthProvider extends GetConnect { - final deviceEndpoint = Uri.parse('/api/notifications/subscribe'); - final tokenEndpoint = Uri.parse('/api/auth/token'); - final userinfoEndpoint = Uri.parse('/api/users/me'); + final deviceEndpoint = Uri.parse('${ServiceFinder.services['passport']}/api/notifications/subscribe'); + final tokenEndpoint = Uri.parse('${ServiceFinder.services['passport']}/api/auth/token'); + final userinfoEndpoint = Uri.parse('${ServiceFinder.services['passport']}/api/users/me'); final redirectUrl = Uri.parse('solian://auth'); static const clientId = 'solian'; @@ -57,9 +56,9 @@ class AuthProvider extends GetConnect { void applyAuthenticator() { isAuthorized.then((status) async { - final content = await storage.read(key: 'auth_credentials'); - credentials = oauth2.Credentials.fromJson(jsonDecode(content!)); if (status) { + final content = await storage.read(key: 'auth_credentials'); + credentials = oauth2.Credentials.fromJson(jsonDecode(content!)); httpClient.addAuthenticator(reqAuthenticator); } }); @@ -96,5 +95,5 @@ class AuthProvider extends GetConnect { Future get isAuthorized => storage.containsKey(key: 'auth_credentials'); - Future> get profile => get('/api/users/me', decoder: (data) => Account.fromJson(data)); + Future getProfile() => get('/api/users/me'); } diff --git a/lib/router.dart b/lib/router.dart index 4ff70ec..9d32169 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,9 +1,11 @@ import 'package:go_router/go_router.dart'; import 'package:solian/screens/account.dart'; +import 'package:solian/screens/auth/signin.dart'; +import 'package:solian/screens/auth/signup.dart'; import 'package:solian/screens/home.dart'; import 'package:solian/shells/nav_shell.dart'; -class AppRouter { +abstract class AppRouter { static GoRouter instance = GoRouter( routes: [ ShellRoute( @@ -19,6 +21,16 @@ class AppRouter { name: "account", builder: (context, state) => const AccountScreen(), ), + GoRoute( + path: "/auth/sign-in", + name: "signin", + builder: (context, state) => const SignInScreen(), + ), + GoRoute( + path: "/auth/sign-up", + name: "signup", + builder: (context, state) => const SignUpScreen(), + ), ], ), ], diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 7030a2a..b6a160f 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -1,12 +1,166 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/router.dart'; +import 'package:solian/widgets/account/account_avatar.dart'; -class AccountScreen extends StatelessWidget { +class AccountScreen extends StatefulWidget { const AccountScreen({super.key}); + @override + State createState() => _AccountScreenState(); +} + +class _AccountScreenState extends State { @override Widget build(BuildContext context) { - return const Center( - child: Text("Woah account"), + final actionItems = [ + // (const Icon(Icons.color_lens), 'personalize'.tr, 'account.personalize'), + // (const Icon(Icons.diversity_1), 'friend'.tr, 'account.friend'), + ]; + + final AuthProvider provider = Get.find(); + + return Material( + color: Theme.of(context).colorScheme.background, + child: FutureBuilder( + future: provider.isAuthorized, + builder: (context, snapshot) { + if (!snapshot.hasData || snapshot.data == false) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ActionCard( + icon: const Icon(Icons.login, color: Colors.white), + title: 'signin'.tr, + caption: 'signinCaption'.tr, + onTap: () { + AppRouter.instance.pushNamed('signin').then((_) { + setState(() {}); + }); + }, + ), + ActionCard( + icon: const Icon(Icons.add, color: Colors.white), + title: 'signup'.tr, + caption: 'signupCaption'.tr, + onTap: () { + AppRouter.instance.pushNamed('signup'); + }, + ), + ], + ), + ); + } + + return Column( + children: [ + const AccountNameCard().paddingOnly(bottom: 8), + ...(actionItems.map( + (x) => ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 34), + leading: x.$1, + title: Text(x.$2), + onTap: () { + AppRouter.instance.pushNamed(x.$3); + }, + ), + )), + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 34), + leading: const Icon(Icons.logout), + title: Text('signout'.tr), + onTap: () { + provider.signout(); + setState(() {}); + }, + ), + ], + ); + }, + ), + ); + } +} + +class AccountNameCard extends StatelessWidget { + const AccountNameCard({super.key}); + + @override + Widget build(BuildContext context) { + final AuthProvider provider = Get.find(); + + return FutureBuilder( + future: provider.getProfile(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + return Material( + elevation: 2, + child: ListTile( + contentPadding: const EdgeInsets.only(left: 22, right: 34, top: 4, bottom: 4), + leading: AccountAvatar(content: snapshot.data!.body?['avatar'], radius: 24), + title: Text(snapshot.data!.body?['nick']), + subtitle: Text(snapshot.data!.body?['name']), + ), + ); + }, + ); + } +} + +class ActionCard extends StatelessWidget { + final Widget icon; + final String title; + final String caption; + final Function onTap; + + const ActionCard({ + super.key, + required this.onTap, + required this.title, + required this.caption, + required this.icon, + }); + + @override + Widget build(BuildContext context) { + return Card( + child: InkWell( + borderRadius: BorderRadius.circular(10), + onTap: () => onTap(), + child: Container( + width: 320, + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + backgroundColor: Colors.indigo, + child: icon, + ).paddingOnly(bottom: 12), + Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + overflow: TextOverflow.clip, + ), + ), + Text( + caption, + style: const TextStyle(overflow: TextOverflow.clip), + ), + ], + ), + ), + ), ); } } diff --git a/lib/screens/auth/signin.dart b/lib/screens/auth/signin.dart new file mode 100644 index 0000000..b7cf678 --- /dev/null +++ b/lib/screens/auth/signin.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/router.dart'; +import 'package:solian/services.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class SignInScreen extends StatefulWidget { + const SignInScreen({super.key}); + + @override + State createState() => _SignInScreenState(); +} + +class _SignInScreenState extends State { + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + + void performAction(BuildContext context) { + final AuthProvider provider = Get.find(); + + final username = _usernameController.value.text; + final password = _passwordController.value.text; + if (username.isEmpty || password.isEmpty) return; + provider.signin(context, username, password).then((_) { + AppRouter.instance.pop(true); + }).catchError((e) { + List messages = e.toString().split('\n'); + if (messages.last.contains('risk')) { + final ticketId = RegExp(r'ticketId=(\d+)').firstMatch(messages.last); + if (ticketId == null) { + Get.showSnackbar(GetSnackBar( + title: 'errorHappened'.tr, + message: 'requested to multi-factor authenticate, but the ticket id was not found', + )); + } + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('riskDetection'.tr), + content: Text('signinRiskDetected'.tr), + actions: [ + TextButton( + child: Text('next'.tr), + onPressed: () { + launchUrlString( + '${ServiceFinder.services['passport']}/mfa?ticket=${ticketId!.group(1)}', + ); + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + }, + ) + ], + ); + }, + ); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(messages.last), + )); + } + }); + } + + @override + Widget build(BuildContext context) { + return Material( + color: Theme.of(context).colorScheme.background, + child: Center( + child: Container( + width: MediaQuery.of(context).size.width * 0.6, + constraints: const BoxConstraints(maxWidth: 360), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Image.asset('assets/logo.png', width: 72, height: 72), + ), + TextField( + autocorrect: false, + enableSuggestions: false, + controller: _usernameController, + autofillHints: const [AutofillHints.username], + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: 'username'.tr, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + const SizedBox(height: 12), + TextField( + obscureText: true, + autocorrect: false, + enableSuggestions: false, + autofillHints: const [AutofillHints.password], + controller: _passwordController, + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: 'password'.tr, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onSubmitted: (_) => performAction(context), + ), + const SizedBox(height: 16), + ElevatedButton( + child: Text('signin'.tr), + onPressed: () => performAction(context), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/auth/signup.dart b/lib/screens/auth/signup.dart new file mode 100644 index 0000000..2ab3806 --- /dev/null +++ b/lib/screens/auth/signup.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:solian/router.dart'; +import 'package:solian/services.dart'; + +class SignUpScreen extends StatefulWidget { + const SignUpScreen({super.key}); + + @override + State createState() => _SignUpScreenState(); +} + +class _SignUpScreenState extends State { + final _emailController = TextEditingController(); + final _usernameController = TextEditingController(); + final _nicknameController = TextEditingController(); + final _passwordController = TextEditingController(); + + void performAction(BuildContext context) async { + final email = _emailController.value.text; + final username = _usernameController.value.text; + final nickname = _passwordController.value.text; + final password = _passwordController.value.text; + if (email.isEmpty || + username.isEmpty || + nickname.isEmpty || + password.isEmpty) return; + + final client = GetConnect(); + client.httpClient.baseUrl = ServiceFinder.services['passport']; + final res = await client.post('/api/users', { + 'name': username, + 'nick': nickname, + 'email': email, + 'password': password, + }); + + if (res.statusCode == 200) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('signupDone'.tr), + content: Text('signupDoneCaption'.tr), + actions: [ + TextButton( + child: Text('confirmOkay'.tr), + onPressed: () => Navigator.pop(context), + ), + ], + ); + }, + ).then((_) { + AppRouter.instance.replaceNamed('auth.sign-in'); + }); + } else { + Get.showSnackbar(GetSnackBar( + title: 'errorHappened'.tr, + message: res.bodyString, + )); + } + } + + @override + Widget build(BuildContext context) { + return Material( + color: Theme.of(context).colorScheme.background, + child: Center( + child: Container( + width: MediaQuery.of(context).size.width * 0.6, + constraints: const BoxConstraints(maxWidth: 360), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Image.asset('assets/logo.png', width: 72, height: 72), + ), + TextField( + autocorrect: false, + enableSuggestions: false, + controller: _usernameController, + autofillHints: const [AutofillHints.username], + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: 'username'.tr, + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + const SizedBox(height: 12), + TextField( + autocorrect: false, + enableSuggestions: false, + controller: _nicknameController, + autofillHints: const [AutofillHints.nickname], + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: 'nickname'.tr, + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + const SizedBox(height: 12), + TextField( + autocorrect: false, + enableSuggestions: false, + controller: _emailController, + autofillHints: const [AutofillHints.email], + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: 'email'.tr, + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + const SizedBox(height: 12), + TextField( + obscureText: true, + autocorrect: false, + enableSuggestions: false, + autofillHints: const [AutofillHints.password], + controller: _passwordController, + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: 'password'.tr, + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + onSubmitted: (_) => performAction(context), + ), + const SizedBox(height: 16), + ElevatedButton( + child: Text('signup'.tr), + onPressed: () => performAction(context), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 7194dc2..711b975 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -52,23 +52,26 @@ class _HomeScreenState extends State { ); } - return RefreshIndicator( - onRefresh: () { - _data.clear(); - _pageKey = 0; - _dataTotal = null; - return getPosts(); - }, - child: ListView.separated( - itemCount: _data.length, - itemBuilder: (BuildContext context, int index) { - final item = _data[index]; - return GestureDetector( - child: PostItem(key: Key('p${item.alias}'), item: item), - onTap: () {}, - ); + return Material( + color: Theme.of(context).colorScheme.background, + child: RefreshIndicator( + onRefresh: () { + _data.clear(); + _pageKey = 0; + _dataTotal = null; + return getPosts(); }, - separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3), + child: ListView.separated( + itemCount: _data.length, + itemBuilder: (BuildContext context, int index) { + final item = _data[index]; + return GestureDetector( + child: PostItem(key: Key('p${item.alias}'), item: item), + onTap: () {}, + ); + }, + separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3), + ), ), ); } diff --git a/lib/shells/nav_shell.dart b/lib/shells/nav_shell.dart index 2960ee1..43d3c6a 100644 --- a/lib/shells/nav_shell.dart +++ b/lib/shells/nav_shell.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:go_router/go_router.dart'; +import 'package:solian/router.dart'; import 'package:solian/theme.dart'; import 'package:solian/widgets/navigation/app_navigation_bottom_bar.dart'; import 'package:solian/widgets/navigation/app_navigation_rail.dart'; @@ -13,12 +14,25 @@ class NavShell extends StatelessWidget { @override Widget build(BuildContext context) { + final backButton = IconButton( + icon: const Icon(Icons.arrow_back), + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + onPressed: () { + if (AppRouter.instance.canPop()) { + AppRouter.instance.pop(); + } + }, + ); + + final canPop = AppRouter.instance.canPop(); + return Scaffold( appBar: AppBar( title: Text(state.topRoute?.name?.tr ?? 'page'.tr), centerTitle: false, + titleSpacing: canPop ? null : 24, elevation: SolianTheme.isLargeScreen(context) ? 1 : 0, - titleSpacing: 24, + leading: canPop ? backButton : null, ), bottomNavigationBar: SolianTheme.isLargeScreen(context) ? null : const AppNavigationBottomBar(), body: SolianTheme.isLargeScreen(context) diff --git a/lib/translations.dart b/lib/translations.dart index 587dd3f..41f6cd2 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -4,16 +4,46 @@ class SolianMessages extends Translations { @override Map> get keys => { 'en_US': { + 'next': 'Next', 'page': 'Page', 'home': 'Home', + 'errorHappened': 'An error occurred', + 'email': 'Email', + 'username': 'Username', + 'nickname': 'Nickname', + 'password': 'Password', 'account': 'Account', + 'personalize': 'Personalize', + 'friend': 'Friend', + 'signin': 'Sign in', + 'signinCaption': 'Sign in to create post, start a realm, message your friend and more!', + 'signinRiskDetected': 'Risk detected, click Next to open a webpage and signin through it to pass security check.', + 'signup': 'Sign up', + 'signupCaption': 'Create an account on Solarpass and then get the access of entire Solar Network!', + 'signout': 'Sign out', + 'riskDetection': 'Risk Detected', 'matureContent': 'Mature Content', 'matureContentCaption': 'The content is rated and may not suitable for everyone to view' }, 'zh_CN': { + 'next': '下一步', 'page': '页面', 'home': '首页', + 'errorHappened': '发生错误了', + 'email': '邮件地址', + 'username': '用户名', + 'nickname': '显示名', + 'password': '密码', 'account': '账号', + 'personalize': '个性化', + 'friend': '好友', + 'signin': '登录', + 'signinCaption': '登录以发表帖子、文章、创建领域、和你的朋友聊天,以及获取更多功能!', + 'signinRiskDetected': '检测到风险,点击下一步按钮来打开一个网页,并通过在其上面登录来通过安全检查。', + 'signup': '注册', + 'signupCaption': '在 Solarpass 注册一个账号以获得整个 Solar Network 的存取权!', + 'signout': '登出', + 'riskDetection': '检测到风险', 'matureContent': '成人内容', 'matureContentCaption': '该内容可能会对您的社会关系产生影响,请确认四下环境后再查看' } diff --git a/lib/widgets/posts/post_item.dart b/lib/widgets/posts/post_item.dart index 0775811..732630f 100644 --- a/lib/widgets/posts/post_item.dart +++ b/lib/widgets/posts/post_item.dart @@ -33,7 +33,7 @@ class _PostItemState extends State { Text( widget.item.author.nick, style: const TextStyle(fontWeight: FontWeight.bold), - ).paddingOnly(left: 8), + ).paddingOnly(left: 12), Text(format(widget.item.createdAt, locale: 'en_short')).paddingOnly(left: 4), ], ), @@ -42,7 +42,7 @@ class _PostItemState extends State { physics: const NeverScrollableScrollPhysics(), data: widget.item.content, padding: const EdgeInsets.all(0), - ).paddingSymmetric(horizontal: 8), + ).paddingOnly(left: 12, right: 8), ], ), ) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d0e7f79..38dd0bc 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b29e9ba..65240e9 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_linux + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 15a1671..5efd5bd 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,8 +7,10 @@ import Foundation import flutter_secure_storage_macos import path_provider_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 4a5d932..d1f6bd1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -453,6 +453,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + url: "https://pub.dev" + source: hosted + version: "6.2.6" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 058330c..5a2c1ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: flutter_secure_storage: ^9.2.1 oauth2: ^2.0.2 carousel_slider: ^4.2.1 + url_launcher: ^6.2.6 dev_dependencies: flutter_test: @@ -67,9 +68,8 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + assets: + - assets/logo.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 0c50753..2048c45 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,11 @@ #include "generated_plugin_registrant.h" #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 4fc759c..de626cc 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_windows + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST