Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion valdi_modules/playground/src/WidgetsCatalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { WithInsets } from 'widgets/src/components/util/WithInsets';
import { DatePicker } from 'widgets/src/components/pickers/DatePicker';
import { TimePicker, TimePickerTime } from 'widgets/src/components/pickers/TimePicker';
import { IndexPicker } from 'widgets/src/components/pickers/IndexPicker';
import { FilePicker, FilePickerOnSelectEvent } from 'widgets/src/components/pickers/FilePicker';
import { EmojiLabel } from 'widgets/src/components/text/EmojiLabel';
import { Section } from 'widgets/src/components/section/Section';
import { SectionSeparator } from 'widgets/src/components/section/SectionSeparator';
Expand All @@ -15,17 +16,19 @@ interface CatalogState {
date: Date;
time: TimePickerTime;
fruitIndex: number;
selectedFileName: string;
}

export class WidgetsCatalog extends StatefulComponent<{}, CatalogState, {}> {
state: CatalogState = {
date: new Date(),
time: { hourOfDay: new Date().getHours(), minuteOfHour: 0 },
fruitIndex: 0,
selectedFileName: '',
};

onRender(): void {
const { date, time, fruitIndex } = this.state;
const { date, time, fruitIndex, selectedFileName } = this.state;
const dateStr = date.toLocaleDateString();
const timeStr = `${String(time.hourOfDay).padStart(2, '0')}:${String(time.minuteOfHour).padStart(2, '0')}`;
const fruitLabel = FRUIT_LABELS[fruitIndex];
Expand Down Expand Up @@ -79,6 +82,20 @@ export class WidgetsCatalog extends StatefulComponent<{}, CatalogState, {}> {

<SectionSeparator />

<Section title='FilePicker'>
<FilePicker
onSelect={(e: FilePickerOnSelectEvent) => this.setState({ selectedFileName: e.fileName })}
/>
<label
value={selectedFileName ? `Selected: ${selectedFileName}` : 'No file selected'}
font={TextStyleFont.BODY}
color={SemanticColor.Text.SECONDARY}
margin='8 0 0 0'
/>
</Section>

<SectionSeparator />

<Section title='EmojiLabel'>
<EmojiLabel value='🎉 Celebration!' font={TextStyleFont.TITLE_2} />
<EmojiLabel value='🌍 Hello World 🌎' font={TextStyleFont.BODY} margin='8 0 0 0' />
Expand Down
12 changes: 6 additions & 6 deletions valdi_modules/widgets/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
load("@valdi//bzl/valdi:valdi_module.bzl", "valdi_module")
load("@valdi//bzl/valdi:valdi_android_library.bzl", "valdi_android_library")

# Web polyglot implementations (DatePicker, TimePicker, IndexPicker, EmojiLabel).
# Web polyglot implementations (DatePicker, TimePicker, IndexPicker, FilePicker, EmojiLabel).
# The build system's generate_register_native_modules picks up the compiled JS
# and checks for webPolyglotViews exports to register custom view factories.
ts_project(
Expand All @@ -13,30 +13,30 @@ ts_project(
visibility = ["//visibility:public"],
)

# Android attribute binders for polyglot custom views (IndexPicker, DatePicker, TimePicker).
# Android attribute binders for polyglot custom views (IndexPicker, DatePicker, TimePicker, FilePicker).
valdi_android_library(
name = "widgets_android",
srcs = glob(["android/*.kt"]),
deps = ["@valdi//valdi:valdi_java"],
)

# iOS native views for polyglot custom views (IndexPicker, DatePicker, TimePicker, EmojiLabel).
# iOS native views for polyglot custom views (IndexPicker, DatePicker, TimePicker, FilePicker, EmojiLabel).
objc_library(
name = "widgets_ios_impl",
srcs = glob(["ios/**/*.m"]),
hdrs = glob(["ios/**/*.h"]),
sdk_frameworks = ["UIKit"],
sdk_frameworks = ["UIKit", "UniformTypeIdentifiers"],
deps = [
"@valdi//valdi_core:valdi_core_objc",
],
)

# macOS native views for polyglot custom views (DatePicker, TimePicker, IndexPicker, EmojiLabel).
# macOS native views for polyglot custom views (DatePicker, TimePicker, IndexPicker, FilePicker, EmojiLabel).
objc_library(
name = "widgets_macos_impl",
srcs = glob(["macos/**/*.m"]),
hdrs = glob(["macos/**/*.h"]),
sdk_frameworks = ["Cocoa"],
sdk_frameworks = ["Cocoa", "UniformTypeIdentifiers"],
deps = ["@valdi//valdi:valdi_macos"],
)

Expand Down
82 changes: 82 additions & 0 deletions valdi_modules/widgets/android/ValdiFilePicker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.snap.widgets.pickers

import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.graphics.Color
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.LinearLayout
import com.snap.valdi.callable.ValdiFunction
import com.snap.valdi.views.ValdiTouchEventResult
import com.snap.valdi.views.ValdiTouchTarget

class ValdiFilePicker(context: Context) : LinearLayout(context), ValdiTouchTarget {

var onSelectFunction: ValdiFunction? = null
var allowMultiple: Boolean = false
var accept: String? = null

private val pickButton: Button

init {
orientation = HORIZONTAL
gravity = Gravity.CENTER
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
minimumHeight = 56
setBackgroundColor(Color.TRANSPARENT)

pickButton = Button(context).apply {
text = "Choose file…"
setOnClickListener { launchFilePicker() }
}
addView(pickButton)
}

private fun resolveActivity(): Activity? {
var ctx: Context? = context
while (ctx != null) {
if (ctx is Activity) return ctx
ctx = (ctx as? ContextWrapper)?.baseContext
}
var v: View? = this
while (v != null) {
var c: Context? = v.context
while (c != null) {
if (c is Activity) return c
c = (c as? ContextWrapper)?.baseContext
}
v = (v.parent as? View)
}
return null
}

private fun launchFilePicker() {
val activity = resolveActivity() ?: return
try {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
type = accept ?: "*/*"
addCategory(Intent.CATEGORY_OPENABLE)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple)
}
activity.startActivity(Intent.createChooser(intent, "Select file"))
} catch (_: Exception) { }
}

override fun processTouchEvent(event: MotionEvent): ValdiTouchEventResult {
val consumed = dispatchTouchEvent(event)
return if (consumed) {
ValdiTouchEventResult.ConsumeEventAndCancelOtherGestures
} else {
ValdiTouchEventResult.IgnoreEvent
}
}
}
33 changes: 33 additions & 0 deletions valdi_modules/widgets/android/ValdiFilePickerAttributesBinder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.snap.widgets.pickers

import android.content.Context
import com.snap.valdi.attributes.AttributesBinder
import com.snap.valdi.attributes.AttributesBindingContext
import com.snap.valdi.attributes.RegisterAttributesBinder

@RegisterAttributesBinder
class ValdiFilePickerAttributesBinder(private val context: Context) : AttributesBinder<ValdiFilePicker> {

override val viewClass: Class<ValdiFilePicker>
get() = ValdiFilePicker::class.java

override fun bindAttributes(attributesBindingContext: AttributesBindingContext<ValdiFilePicker>) {
attributesBindingContext.bindFunctionAttribute("onSelect", { view, fn ->
view.onSelectFunction = fn
}, { view ->
view.onSelectFunction = null
})

attributesBindingContext.bindBooleanAttribute("allowMultiple", false, { view, value, _ ->
view.allowMultiple = value
}, { view, _ ->
view.allowMultiple = false
})

attributesBindingContext.bindStringAttribute("accept", false, { view, value, _ ->
view.accept = value
}, { view, _ ->
view.accept = null
})
}
}
9 changes: 9 additions & 0 deletions valdi_modules/widgets/ios/SCWidgetsFilePicker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface SCWidgetsFilePicker : UIView

@end

NS_ASSUME_NONNULL_END
125 changes: 125 additions & 0 deletions valdi_modules/widgets/ios/SCWidgetsFilePicker.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#import "SCWidgetsFilePicker.h"

#import "valdi_core/SCValdiAttributesBinderBase.h"
#import "valdi_core/SCValdiFunction.h"
#import "valdi_core/SCValdiMarshaller.h"

#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>

@interface SCWidgetsFilePicker () <UIDocumentPickerDelegate>
@property (nonatomic) UIButton *pickButton;
@property (nonatomic) id<SCValdiFunction> onSelect;
@property (nonatomic) BOOL allowMultiple;
@property (nonatomic, copy, nullable) NSString *accept;
@end

@implementation SCWidgetsFilePicker

- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_pickButton = [UIButton buttonWithType:UIButtonTypeSystem];
[_pickButton setTitle:@"Choose file…" forState:UIControlStateNormal];
[_pickButton addTarget:self action:@selector(_openPicker) forControlEvents:UIControlEventTouchUpInside];
_pickButton.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_pickButton];

[NSLayoutConstraint activateConstraints:@[
[_pickButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[_pickButton.centerYAnchor constraintEqualToAnchor:self.centerYAnchor],
]];
}
return self;
}

- (CGSize)sizeThatFits:(CGSize)size {
return CGSizeMake(size.width, MAX(56, size.height));
}

#pragma mark - File picker

- (void)_openPicker {
NSArray<UTType *> *types = [self _resolveContentTypes];
UIDocumentPickerViewController *picker =
[[UIDocumentPickerViewController alloc] initForOpeningContentTypes:types];
picker.allowsMultipleSelection = self.allowMultiple;
picker.delegate = self;

UIViewController *vc = [self _findViewController];
if (vc) {
[vc presentViewController:picker animated:YES completion:nil];
}
}

- (NSArray<UTType *> *)_resolveContentTypes {
if (self.accept.length == 0) {
return @[UTTypeItem];
}
NSMutableArray<UTType *> *types = [NSMutableArray array];
for (NSString *raw in [self.accept componentsSeparatedByString:@","]) {
NSString *trimmed = [raw stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
UTType *type = [UTType typeWithMIMEType:trimmed];
if (type) {
[types addObject:type];
}
}
return types.count > 0 ? types : @[UTTypeItem];
}

- (UIViewController *)_findViewController {
UIResponder *responder = self;
while (responder) {
if ([responder isKindOfClass:[UIViewController class]]) {
return (UIViewController *)responder;
}
responder = responder.nextResponder;
}
return nil;
}

#pragma mark - UIDocumentPickerDelegate

- (void)documentPicker:(UIDocumentPickerViewController *)controller
didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
if (!self.onSelect) return;
for (NSURL *url in urls) {
SCValdiMarshallerScoped(marshaller, {
NSInteger objectIndex = SCValdiMarshallerPushMap(marshaller, 1);
SCValdiMarshallerPushString(marshaller, url.lastPathComponent);
SCValdiMarshallerPutMapPropertyUninterned(marshaller, @"fileName", objectIndex);
[self.onSelect performWithMarshaller:marshaller];
});
}
}

#pragma mark - Attribute binding

+ (void)bindAttributes:(id<SCValdiAttributesBinderProtocol>)attributesBinder {
[attributesBinder bindAttribute:@"onSelect"
withFunctionBlock:^(SCWidgetsFilePicker *view, id<SCValdiFunction> fn) {
view.onSelect = fn;
}
resetBlock:^(SCWidgetsFilePicker *view) {
view.onSelect = nil;
}];

[attributesBinder bindAttribute:@"allowMultiple"
invalidateLayoutOnChange:NO
withBoolBlock:^BOOL(SCWidgetsFilePicker *view, BOOL value, id<SCValdiAnimatorProtocol> animator) {
view.allowMultiple = value;
return YES;
} resetBlock:^(SCWidgetsFilePicker *view, id<SCValdiAnimatorProtocol> animator) {
view.allowMultiple = NO;
}];

[attributesBinder bindAttribute:@"accept"
invalidateLayoutOnChange:NO
withStringBlock:^BOOL(SCWidgetsFilePicker *view, NSString *value, id<SCValdiAnimatorProtocol> animator) {
view.accept = value;
return YES;
} resetBlock:^(SCWidgetsFilePicker *view, id<SCValdiAnimatorProtocol> animator) {
view.accept = nil;
}];
}

@end
12 changes: 12 additions & 0 deletions valdi_modules/widgets/macos/SCWidgetsMacOSFilePicker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#import "valdi/macos/SCValdiMacOSAttributesBinder.h"
#import <Cocoa/Cocoa.h>

NS_ASSUME_NONNULL_BEGIN

@interface SCWidgetsMacOSFilePicker : NSView

+ (void)bindAttributes:(SCValdiMacOSAttributesBinder *)attributesBinder;

@end

NS_ASSUME_NONNULL_END
Loading
Loading