diff --git a/ios/Podfile b/ios/Podfile index e549ee2..c4cb422 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -30,6 +30,8 @@ flutter_ios_podfile_setup target 'Runner' do use_frameworks! + pod 'Kingfisher', '~> 8.0' + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ecc1c54..87ad3ea 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,30 +1,56 @@ PODS: - Flutter (1.0.0) + - Kingfisher (8.3.1) + - native_video_player (1.0.0): + - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + - url_launcher_ios (0.0.1): + - Flutter DEPENDENCIES: - Flutter (from `Flutter`) + - Kingfisher (~> 8.0) + - native_video_player (from `.symlinks/plugins/native_video_player/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +SPEC REPOS: + trunk: + - Kingfisher EXTERNAL SOURCES: Flutter: :path: Flutter + native_video_player: + :path: ".symlinks/plugins/native_video_player/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + Kingfisher: 3204d23de16b5ea53541c44ca5a8efb55741dec3 + native_video_player: e363dd14f6a498ad8a8f7e6486a0db046ad19f13 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d -PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5 +PODFILE CHECKSUM: b9d63df345d0c6f260ddc467a83ae5f9c8a6cd6f COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 568d6a8..ed3f311 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -52,6 +52,7 @@ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3A1C47BD29CC6AC2587D4DBE /* 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 = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -67,6 +68,10 @@ F6D834CA86410B09796B312B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 737E91D52DB6866B00BE9CDB /* NativeViews */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NativeViews; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 1DFF8FEBBD0CF5A990600776 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -114,7 +119,6 @@ 14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */, E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -153,6 +157,8 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */, + 737E91D52DB6866B00BE9CDB /* NativeViews */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, @@ -204,6 +210,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 737E91D52DB6866B00BE9CDB /* NativeViews */, + ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; @@ -316,10 +325,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -470,10 +483,12 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = W7HPZ53V6B; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -653,10 +668,12 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = W7HPZ53V6B; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -676,10 +693,12 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = W7HPZ53V6B; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 6266644..c3c2bdb 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -3,11 +3,16 @@ import UIKit @main @objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + + guard let pluginRegistrar = self.registrar(forPlugin: "plugin-name") else { return false } + + pluginRegistrar.register(FLNativeImageFactory(messenger: pluginRegistrar.messenger()), withId: "native-image") + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f455cb0..f94f4b3 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -24,6 +26,14 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + fetch + audio + remote-notification + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,9 +51,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/ios/Runner/NativeViews/BlurHashDecoder.swift b/ios/Runner/NativeViews/BlurHashDecoder.swift new file mode 100644 index 0000000..017ae83 --- /dev/null +++ b/ios/Runner/NativeViews/BlurHashDecoder.swift @@ -0,0 +1,153 @@ +// +// BlurHashDecoder.swift +// Runner +// +// Created by LittleSheep on 2025/4/21. +// + +import UIKit + +extension UIImage { + public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) { + guard blurHash.count >= 6 else { return nil } + + let sizeFlag = String(blurHash[0]).decode83() + let numY = (sizeFlag / 9) + 1 + let numX = (sizeFlag % 9) + 1 + + let quantisedMaximumValue = String(blurHash[1]).decode83() + let maximumValue = Float(quantisedMaximumValue + 1) / 166 + + guard blurHash.count == 4 + 2 * numX * numY else { return nil } + + let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in + if i == 0 { + let value = String(blurHash[2 ..< 6]).decode83() + return decodeDC(value) + } else { + let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83() + return decodeAC(value, maximumValue: maximumValue * punch) + } + } + + let width = Int(size.width) + let height = Int(size.height) + let bytesPerRow = width * 3 + guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil } + CFDataSetLength(data, bytesPerRow * height) + guard let pixels = CFDataGetMutableBytePtr(data) else { return nil } + + for y in 0 ..< height { + for x in 0 ..< width { + var r: Float = 0 + var g: Float = 0 + var b: Float = 0 + + for j in 0 ..< numY { + for i in 0 ..< numX { + let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height)) + let colour = colours[i + j * numX] + r += colour.0 * basis + g += colour.1 * basis + b += colour.2 * basis + } + } + + let intR = UInt8(linearTosRGB(r)) + let intG = UInt8(linearTosRGB(g)) + let intB = UInt8(linearTosRGB(b)) + + pixels[3 * x + 0 + y * bytesPerRow] = intR + pixels[3 * x + 1 + y * bytesPerRow] = intG + pixels[3 * x + 2 + y * bytesPerRow] = intB + } + } + + let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue) + + guard let provider = CGDataProvider(data: data) else { return nil } + guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil } + + self.init(cgImage: cgImage) + } +} + +private func decodeDC(_ value: Int) -> (Float, Float, Float) { + let intR = value >> 16 + let intG = (value >> 8) & 255 + let intB = value & 255 + return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB)) +} + +private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) { + let quantR = value / (19 * 19) + let quantG = (value / 19) % 19 + let quantB = value % 19 + + let rgb = ( + signPow((Float(quantR) - 9) / 9, 2) * maximumValue, + signPow((Float(quantG) - 9) / 9, 2) * maximumValue, + signPow((Float(quantB) - 9) / 9, 2) * maximumValue + ) + + return rgb +} + +private func signPow(_ value: Float, _ exp: Float) -> Float { + return copysign(pow(abs(value), exp), value) +} + +private func linearTosRGB(_ value: Float) -> Int { + let v = max(0, min(1, value)) + if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } + else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) } +} + +private func sRGBToLinear(_ value: Type) -> Float { + let v = Float(Int64(value)) / 255 + if v <= 0.04045 { return v / 12.92 } + else { return pow((v + 0.055) / 1.055, 2.4) } +} + +private let encodeCharacters: [String] = { + return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) } +}() + +private let decodeCharacters: [String: Int] = { + var dict: [String: Int] = [:] + for (index, character) in encodeCharacters.enumerated() { + dict[character] = index + } + return dict +}() + +extension String { + func decode83() -> Int { + var value: Int = 0 + for character in self { + if let digit = decodeCharacters[String(character)] { + value = value * 83 + digit + } + } + return value + } +} + +private extension String { + subscript (offset: Int) -> Character { + return self[index(startIndex, offsetBy: offset)] + } + + subscript (bounds: CountableClosedRange) -> Substring { + let start = index(startIndex, offsetBy: bounds.lowerBound) + let end = index(startIndex, offsetBy: bounds.upperBound) + return self[start...end] + } + + subscript (bounds: CountableRange) -> Substring { + let start = index(startIndex, offsetBy: bounds.lowerBound) + let end = index(startIndex, offsetBy: bounds.upperBound) + return self[start.. RoundCornerImageProcessor(cornerRadius: 20) + + self.kf.indicatorType = .activity + self.kf.setImage( + with: url, + placeholder: placeholderImage, + options: [ + .processor(processor), + .transition(.fade(0.3)) + ] + ) + } +} diff --git a/ios/Runner/NativeViews/NativeImage.swift b/ios/Runner/NativeViews/NativeImage.swift new file mode 100644 index 0000000..9775582 --- /dev/null +++ b/ios/Runner/NativeViews/NativeImage.swift @@ -0,0 +1,62 @@ +// +// NativeImage.swift +// Runner +// +// Created by LittleSheep on 2025/4/21. +// + +import Flutter +import UIKit +import Kingfisher + +class FLNativeImageFactory : NSObject, FlutterPlatformViewFactory { + private var messenger: FlutterBinaryMessenger + + init(messenger: FlutterBinaryMessenger) { + self.messenger = messenger + super.init() + } + + func create( + withFrame frame: CGRect, + viewIdentifier viewId: Int64, + arguments args: Any? + ) -> FlutterPlatformView { + return FLNativeImage( + frame: frame, + viewIdentifier: viewId, + arguments: args, + binaryMessenger: messenger) + } + + /// Implementing this method is only necessary when the `arguments` in `createWithFrame` is not `nil`. + public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { + return FlutterStandardMessageCodec.sharedInstance() + } +} + +class FLNativeImage : NSObject, FlutterPlatformView { + private var _view: ImageView + + init( + frame: CGRect, + viewIdentifier viewId: Int64, + arguments args: Any?, + binaryMessenger messenger: FlutterBinaryMessenger? + ) { + _view = ImageView(frame: frame) + super.init() + + let argsMap = args as! [AnyHashable: Any] + let source = argsMap["src"] as! String + let blurHash = argsMap["blur"] as? String + + if let url = URL(string: source) { + _view.setImage(from: url, blurHash: blurHash) + } + } + + func view() -> UIView { + return _view + } +} diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..903def2 --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 68b6e38..ad83676 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/models/post.dart'; +import 'package:island/widgets/post/post_item.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:dio/dio.dart'; import 'package:island/pods/network.dart'; @@ -20,14 +21,16 @@ class ExploreScreen extends ConsumerWidget { body: postAsync.when( data: (controller) => InfiniteList( + padding: EdgeInsets.zero, itemCount: controller.posts.length, isLoading: controller.isLoading, hasReachedMax: controller.hasReachedMax, onFetchData: controller.fetchMore, itemBuilder: (context, index) { final post = controller.posts[index]; - return ListTile(title: Text(post.content)); + return PostItem(item: post); }, + separatorBuilder: (_, __) => const Divider(height: 1), ), loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center(child: Text('Error: $e')), diff --git a/lib/widgets/content/cloud_files.dart b/lib/widgets/content/cloud_files.dart new file mode 100644 index 0000000..d439e06 --- /dev/null +++ b/lib/widgets/content/cloud_files.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:island/models/file.dart'; +import 'package:island/pods/config.dart'; + +import 'image.dart'; +import 'video.dart'; + +class CloudFileWidget extends ConsumerWidget { + final SnCloudFile item; + const CloudFileWidget({super.key, required this.item}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final serverUrl = ref.watch(serverUrlProvider); + final uri = '$serverUrl/files/${item.id}'; + switch (item.mimeType?.split('/').firstOrNull) { + case "image": + return AspectRatio( + aspectRatio: (item.fileMeta?['ratio'] ?? 1).toDouble(), + child: UniversalImage(uri: uri, blurHash: item.fileMeta?['blur']), + ); + case "video": + return AspectRatio( + aspectRatio: (item.fileMeta?['ratio'] ?? 16 / 9).toDouble(), + child: UniversalVideo(uri: uri), + ); + default: + return Placeholder(); + } + } +} diff --git a/lib/widgets/content/image.dart b/lib/widgets/content/image.dart new file mode 100644 index 0000000..33291e3 --- /dev/null +++ b/lib/widgets/content/image.dart @@ -0,0 +1 @@ +export 'image.native.dart' if (dart.library.html) 'image.web.dart'; diff --git a/lib/widgets/content/image.native.dart b/lib/widgets/content/image.native.dart new file mode 100644 index 0000000..29e540d --- /dev/null +++ b/lib/widgets/content/image.native.dart @@ -0,0 +1,33 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:flutter/material.dart'; + +class UniversalImage extends StatelessWidget { + final String uri; + final String? blurHash; + const UniversalImage({super.key, required this.uri, this.blurHash}); + + @override + Widget build(BuildContext context) { + final params = {'src': uri, 'blur': blurHash}; + if (Platform.isAndroid) { + return AndroidView( + viewType: 'native-image', + layoutDirection: TextDirection.ltr, + creationParams: params, + creationParamsCodec: const StandardMessageCodec(), + ); + } + if (Platform.isIOS) { + // For iOS: Use UiKitView to embed a native iOS image view + return UiKitView( + viewType: 'native-image', + layoutDirection: TextDirection.ltr, + creationParams: params, + creationParamsCodec: const StandardMessageCodec(), + ); + } + return Image.network(uri); + } +} diff --git a/lib/widgets/content/image.web.dart b/lib/widgets/content/image.web.dart new file mode 100644 index 0000000..2b45f3a --- /dev/null +++ b/lib/widgets/content/image.web.dart @@ -0,0 +1,18 @@ +import 'package:web/web.dart' as web; +import 'package:flutter/material.dart'; + +class UniversalImage extends StatelessWidget { + final String uri; + const UniversalImage({super.key, required this.uri}); + + @override + Widget build(BuildContext context) { + return HtmlElementView( + viewType: 'native-image', + onPlatformViewCreated: (int viewId) { + final element = web.HTMLImageElement()..src = uri; + web.document.body!.append(element); + }, + ); + } +} diff --git a/lib/widgets/content/markdown.dart b/lib/widgets/content/markdown.dart new file mode 100644 index 0000000..1ae2b3b --- /dev/null +++ b/lib/widgets/content/markdown.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_highlight/flutter_highlight.dart'; +import 'package:flutter_highlight/theme_map.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_markdown_latex/flutter_markdown_latex.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:markdown/markdown.dart' as markdown; +import 'package:url_launcher/url_launcher_string.dart'; + +class MarkdownTextContent extends StatelessWidget { + final String content; + final bool isAutoWarp; + final bool isEnlargeSticker; + final TextScaler? textScaler; + final Color? textColor; + + const MarkdownTextContent({ + super.key, + required this.content, + this.isAutoWarp = false, + this.isEnlargeSticker = false, + this.textScaler, + this.textColor, + }); + + @override + Widget build(BuildContext context) { + return Markdown( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + data: content, + padding: EdgeInsets.zero, + styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith( + textScaler: textScaler, + p: + textColor != null + ? Theme.of( + context, + ).textTheme.bodyMedium!.copyWith(color: textColor) + : null, + blockquote: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + blockquoteDecoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + horizontalRuleDecoration: BoxDecoration( + border: Border( + top: BorderSide(width: 1.0, color: Theme.of(context).dividerColor), + ), + ), + codeblockDecoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor, width: 0.3), + borderRadius: const BorderRadius.all(Radius.circular(4)), + color: Theme.of(context).colorScheme.surface.withOpacity(0.5), + ), + code: GoogleFonts.robotoMono(height: 1), + ), + builders: {'latex': LatexElementBuilder(), 'code': HighlightBuilder()}, + softLineBreak: true, + extensionSet: markdown.ExtensionSet( + [ + ...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, + markdown.CodeBlockSyntax(), + markdown.FencedCodeBlockSyntax(), + LatexBlockSyntax(), + ], + [ + ...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes, + if (isAutoWarp) markdown.LineBreakSyntax(), + _UserNameCardInlineSyntax(), + markdown.AutolinkSyntax(), + markdown.AutolinkExtensionSyntax(), + markdown.CodeSyntax(), + LatexInlineSyntax(), + ], + ), + onTapLink: (text, href, title) async { + if (href == null) return; + await launchUrlString(href, mode: LaunchMode.externalApplication); + }, + ); + } +} + +class _UserNameCardInlineSyntax extends markdown.InlineSyntax { + _UserNameCardInlineSyntax() : super(r'@[a-zA-Z0-9_]+'); + + @override + bool onMatch(markdown.InlineParser parser, Match match) { + final alias = match[0]!; + final anchor = markdown.Element.text('a', alias) + ..attributes['href'] = Uri.encodeFull( + 'solink://accounts/${alias.substring(1)}', + ); + parser.addNode(anchor); + + return true; + } +} + +class HighlightBuilder extends MarkdownElementBuilder { + @override + Widget? visitElementAfterWithContext( + BuildContext context, + markdown.Element element, + TextStyle? preferredStyle, + TextStyle? parentStyle, + ) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + if (element.attributes['class'] == null && + !element.textContent.trim().contains('\n')) { + return Container( + padding: EdgeInsets.only(top: 0.0, right: 4.0, bottom: 1.75, left: 4.0), + margin: EdgeInsets.symmetric(horizontal: 2.0), + color: Colors.black12, + child: Text( + element.textContent, + style: GoogleFonts.robotoMono(textStyle: preferredStyle), + ), + ); + } else { + var language = 'plaintext'; + final pattern = RegExp(r'^language-(.+)$'); + if (element.attributes['class'] != null && + pattern.hasMatch(element.attributes['class'] ?? '')) { + language = + pattern.firstMatch(element.attributes['class'] ?? '')?.group(1) ?? + 'plaintext'; + } + return ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: HighlightView( + element.textContent.trim(), + language: language, + theme: { + ...(isDark ? themeMap['a11y-dark']! : themeMap['a11y-light']!), + 'root': + (isDark + ? TextStyle( + backgroundColor: Colors.transparent, + color: Color(0xfff8f8f2), + ) + : TextStyle( + backgroundColor: Colors.transparent, + color: Color(0xff545454), + )), + }, + padding: EdgeInsets.all(12), + textStyle: GoogleFonts.robotoMono(textStyle: preferredStyle), + ), + ); + } + } +} diff --git a/lib/widgets/content/video.dart b/lib/widgets/content/video.dart new file mode 100644 index 0000000..724b3fa --- /dev/null +++ b/lib/widgets/content/video.dart @@ -0,0 +1 @@ +export 'video.native.dart' if (dart.library.html) 'video.web.dart'; diff --git a/lib/widgets/content/video.native.dart b/lib/widgets/content/video.native.dart new file mode 100644 index 0000000..738e71b --- /dev/null +++ b/lib/widgets/content/video.native.dart @@ -0,0 +1,65 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:native_video_player/native_video_player.dart'; + +class UniversalVideo extends StatefulWidget { + final String uri; + const UniversalVideo({super.key, required this.uri}); + + @override + State createState() => _UniversalVideoState(); +} + +class _UniversalVideoState extends State { + NativeVideoPlayerController? _controller; + bool _isPlaying = false; + + Future _togglePlayback() async { + final controller = _controller; + if (controller == null) return; + + if (_isPlaying) { + await controller.pause(); + } else { + await controller.play(); + } + + final isPlaying = await controller.isPlaying(); + setState(() { + _isPlaying = isPlaying; + }); + } + + @override + Widget build(BuildContext context) { + if (Platform.isAndroid || Platform.isIOS) { + return Stack( + children: [ + NativeVideoPlayerView( + onViewReady: (controller) async { + _controller = controller; + await controller.loadVideo( + VideoSource(path: widget.uri, type: VideoSourceType.network), + ); + }, + ), + Material( + type: MaterialType.transparency, + child: InkWell( + onTap: _togglePlayback, + child: Center( + child: Icon( + _isPlaying ? Icons.pause : Icons.play_arrow, + size: 64, + color: Colors.white, + ), + ), + ), + ), + ], + ); + } + return Image.network(widget.uri); + } +} diff --git a/lib/widgets/content/video.web.dart b/lib/widgets/content/video.web.dart new file mode 100644 index 0000000..dc311d7 --- /dev/null +++ b/lib/widgets/content/video.web.dart @@ -0,0 +1,19 @@ +import 'package:web/web.dart' as web; +import 'package:flutter/material.dart'; + +class UniversalVideo extends StatelessWidget { + final String uri; + const UniversalVideo({super.key, required this.uri}); + + @override + Widget build(BuildContext context) { + return HtmlElementView( + viewType: 'native-video', + onPlatformViewCreated: (int viewId) { + final element = web.HTMLVideoElement()..src = uri; + element.controls = true; + web.document.body!.append(element); + }, + ); + } +} diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart new file mode 100644 index 0000000..93cd21b --- /dev/null +++ b/lib/widgets/post/post_item.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:island/models/post.dart'; +import 'package:island/widgets/content/cloud_files.dart'; +import 'package:island/widgets/content/markdown.dart'; +import 'package:styled_widget/styled_widget.dart'; + +class PostItem extends StatelessWidget { + final SnPost item; + final EdgeInsets? padding; + const PostItem({super.key, required this.item, this.padding}); + + @override + Widget build(BuildContext context) { + final renderingPadding = + padding ?? EdgeInsets.symmetric(horizontal: 12, vertical: 16); + + return Padding( + padding: renderingPadding, + child: Column( + children: [ + Row( + children: [ + // Avatar... + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(item.publisher.name).bold(), + if (item.content.isNotEmpty) + MarkdownTextContent(content: item.content), + ], + ), + ), + ], + ), + for (final attachment in item.attachments) + CloudFileWidget(item: attachment), + ], + ), + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index bc8dc59..bb3345d 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) bitsdojo_window_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin"); bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_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 cfdc298..9674f4f 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_linux + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b61c560..a18a921 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,9 +8,13 @@ import Foundation import bitsdojo_window_macos import path_provider_foundation import shared_preferences_foundation +import sqflite_darwin +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 063791c..7fc0899 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -161,6 +161,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.9.5" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -286,6 +310,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_highlight: + dependency: "direct main" + description: + name: flutter_highlight + sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" + url: "https://pub.dev" + source: hosted + version: "0.7.0" flutter_hooks: dependency: "direct main" description: @@ -302,6 +342,30 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + sha256: "634622a3a826d67cb05c0e3e576d1812c430fa98404e95b60b131775c73d76ec" + url: "https://pub.dev" + source: hosted + version: "0.7.7" + flutter_markdown_latex: + dependency: "direct main" + description: + name: flutter_markdown_latex + sha256: "839e76a84abb3632ffcebbd450cf93c7e9894af65622527d23f0084cee1bfd04" + url: "https://pub.dev" + source: hosted + version: "0.3.4" + flutter_math_fork: + dependency: transitive + description: + name: flutter_math_fork + sha256: "284bab89b2fbf1bc3a0baf13d011c1dd324d004e35d177626b77f2fc056366ac" + url: "https://pub.dev" + source: hosted + version: "0.7.3" flutter_riverpod: dependency: "direct main" description: @@ -310,6 +374,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 + url: "https://pub.dev" + source: hosted + version: "2.1.0" flutter_test: dependency: "direct dev" description: flutter @@ -344,6 +416,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + gap: + dependency: "direct main" + description: + name: gap + sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d + url: "https://pub.dev" + source: hosted + version: "3.0.1" glob: dependency: transitive description: @@ -352,6 +432,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" graphs: dependency: transitive description: @@ -360,6 +448,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + highlight: + dependency: transitive + description: + name: highlight + sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" + url: "https://pub.dev" + source: hosted + version: "0.7.0" hooks_riverpod: dependency: "direct main" description: @@ -472,6 +568,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + markdown: + dependency: "direct main" + description: + name: markdown + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + url: "https://pub.dev" + source: hosted + version: "7.3.0" matcher: dependency: transitive description: @@ -504,6 +608,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + native_video_player: + dependency: "direct main" + description: + name: native_video_player + sha256: "64ac4086c50f13306c7ebca70372b2c2c67c063caae25f0c486dbec16d666e9a" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_config: dependency: transitive description: @@ -520,6 +648,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -600,6 +736,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310" + url: "https://pub.dev" + source: hosted + version: "6.1.4" pub_semver: dependency: transitive description: @@ -632,6 +776,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shared_preferences: dependency: "direct main" description: @@ -733,6 +885,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" + url: "https://pub.dev" + source: hosted + version: "2.5.5" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -781,6 +981,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" + url: "https://pub.dev" + source: hosted + version: "3.3.1" term_glyph: dependency: transitive description: @@ -805,6 +1013,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: @@ -813,6 +1029,102 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + 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: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + url: "https://pub.dev" + source: hosted + version: "1.1.18" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + url: "https://pub.dev" + source: hosted + version: "1.1.16" vector_math: dependency: transitive description: @@ -846,7 +1158,7 @@ packages: source: hosted version: "1.1.1" web: - dependency: transitive + dependency: "direct main" description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" diff --git a/pubspec.yaml b/pubspec.yaml index 52d2166..cb8fad7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,17 @@ dependencies: very_good_infinite_list: ^0.9.0 freezed_annotation: ^3.0.0 json_annotation: ^4.9.0 + flutter_markdown_latex: ^0.3.4 + flutter_markdown: ^0.7.7 + markdown: ^7.3.0 + flutter_highlight: ^0.7.0 + uuid: ^4.5.1 + url_launcher: ^6.3.1 + google_fonts: ^6.2.1 + gap: ^3.0.1 + cached_network_image: ^3.4.1 + web: ^1.1.1 + native_video_player: ^3.0.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index ac4badd..4ada1f5 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) { BitsdojoWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index fe90529..72a37b6 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_windows + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST