Companion theme tab, Watch Complications WIP

master
Auri 2021-09-14 18:26:22 -07:00
parent 83e6ee1465
commit d176fb65ab
35 changed files with 1008 additions and 6879 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules
watch/Focus.wgt
watch/build

View File

@ -141,7 +141,7 @@ android {
versionName "1.0.0"
ndk {
abiFilters 'armeabi-v7a'
abiFilters 'armeabi-v7a', 'x86', 'armeabi-v8a', 'x86_64'
}
}
splits {
@ -197,6 +197,8 @@ dependencies {
implementation "com.android.support:appcompat-v7:$supportLibVersion"
implementation "com.android.support:recyclerview-v7:$supportLibVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation files('libs/accessory-v2.6.4.jar')
implementation files('libs/sdk-v1.0.0.jar')
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
}

Binary file not shown.

Binary file not shown.

View File

@ -1,21 +1,31 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.aurailus.focuscompanion">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="com.samsung.WATCH_APP_TYPE.Companion"/>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:usesCleartextTraffic="true">
<meta-data android:name="AccessoryServicesLocation" android:value="/res/xml/accessoryservices.xml"/>
<meta-data android:name="gear_app_packagename" android:value="com.aurailus.focus"/>
<meta-data android:name="GearAppType" android:value="wgt"/>
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
<meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="42.0.0"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://exp.host/@anonymous/FocusCompanion"/>
<service android:name="com.samsung.android.sdk.accessory.SAService"/>
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
@ -24,6 +34,18 @@
<data android:scheme="com.aurailus.focuscompanion"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
<receiver android:exported="false" android:name="com.samsung.android.sdk.accessory.RegisterUponInstallReceiver" >
<intent-filter>
<action android:name="com.samsung.accessory.action.REGISTER_AGENT"/>
</intent-filter>
</receiver>
<receiver android:exported="false" android:name="com.samsung.android.sdk.accessory.ServiceConnectionIndicationBroadcastReceiver" >
<intent-filter>
<action android:name="com.samsung.accessory.action.SERVICE_CONNECTION_REQUESTED"/>
</intent-filter>
</receiver>
</application>
</manifest>
</manifest>

View File

@ -0,0 +1,80 @@
package com.aurailus.focuscompanion
import android.content.Context
import android.util.Log
import android.widget.Toast
import com.samsung.android.sdk.accessory.*
import com.samsung.android.sdk.accessory.SAPeerAgent
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.*
class Service(private val context: Context) :
SAAgentV2(TAG, context) {
private val mMessage = Message(this);
companion object {
private const val TAG = "SyncAP"
}
init {
Toast.makeText(context, "Hello world", Toast.LENGTH_SHORT).show()
val mAccessory = SA()
try {
mAccessory.initialize(context)
}
catch (e: Exception) {
e.printStackTrace()
releaseAgent()
}
}
override fun onFindPeerAgentsResponse(peers: Array<SAPeerAgent>, result: Int) {
Log.d(TAG, "onFindPeerAgentResponse: result = $result")
}
// override fun onError(peer: SAPeerAgent, message: String, code: Int) {
// super.onError(peer, message, code)
// }
// fun sendData(peer: SAPeerAgent, message: String) {
// Thread {
//// try {
// val tid: Int = mMessage.send(peer, message.toByteArray())
//// addMessage("Sent: ", "$message($tid)")
//// } catch (e: IOException) {
//// e.printStackTrace()
//// addMessage("Exception: ", e.getMessage())
//// } catch (e: IllegalArgumentException) {
//// e.printStackTrace()
//// addMessage("Exception: ", e.message)
//// }
// }.start()
// }
inner class Message(private val agent: Service): SAMessage(agent) {
override fun onSent(peer: SAPeerAgent, id: Int) {
Log.d(TAG, "onSent(), id: " + id + ", ToAgent: " + peer.peerId);
}
override fun onError(peer: SAPeerAgent, id: Int, errorCode: Int) {
Log.d(TAG, "onError(), id: " + id + ", ToAgent: " + peer.peerId + ", errorCode: " + errorCode);
}
override fun onReceive(peer: SAPeerAgent, message: ByteArray) {
Log.d(TAG, "onReceive(), FromAgent : " + peer.peerId + " Message : " + String(message));
val calendar = GregorianCalendar();
val dateFormat = SimpleDateFormat("yyyy.MM.dd aa hh:mm:ss.SSS");
val timeStr = " " + dateFormat.format(calendar.time);
val strToUpdateUI = String(message);
// addMessage("Received: ", strToUpdateUI);
// final String str = strToUpdateUI.concat(timeStr);
// sendData(peerAgent, str);
Toast.makeText(agent.context, strToUpdateUI, Toast.LENGTH_SHORT).show()
}
}
}

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<resources>
<string name="app_name">FocusCompanion</string>
<string name="app_name">Focus Companion</string>
<string name="preferences_key">com.aurailus.focuscompanion.PREFERENCE_FILE_KEY</string>
</resources>

View File

@ -0,0 +1,23 @@
<resources>
<application name="MyApplication">
<serviceProfile
id="/com/aurailus/focus/SyncAP"
name="SyncAP"
role="provider"
serviceImpl="com.aurailus.focuscompanion.Service"
version="1.0"
serviceLimit="ANY"
serviceTimeout="10">
<supportedTransports>
<transport type="TRANSPORT_BT" />
<transport type="TRANSPORT_WIFI" />
</supportedTransports>
<serviceChannel
id="110"
dataRate="low"
priority="low"
reliability= "enable"
/>
</serviceProfile>
</application>
</resources>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 87 KiB

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@
},
"dependencies": {
"@expo/vector-icons": "^12.0.0",
"@react-native-picker/picker": "^2.1.0",
"@react-navigation/bottom-tabs": "^6.0.5",
"@react-navigation/material-bottom-tabs": "^6.0.5",
"@react-navigation/native": "^6.0.2",
@ -16,19 +17,23 @@
"expo-linking": "~2.3.1",
"expo-splash-screen": "~0.11.2",
"expo-status-bar": "~1.0.4",
"expo-updates": "~0.8.1",
"expo-web-browser": "~9.2.0",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "~0.63.4",
"react-native-animatable": "^1.3.3",
"react-native-color-picker": "^0.6.0",
"react-native-gesture-handler": "~1.10.2",
"react-native-modal": "^13.0.0",
"react-native-modal-selector": "^2.1.0",
"react-native-paper": "^4.9.2",
"react-native-reanimated": "~2.2.0",
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "~3.4.0",
"react-native-svg": "^12.1.1",
"react-native-web": "~0.13.12",
"expo-updates": "~0.8.1",
"react-native-unimodules": "~0.14.5"
"react-native-unimodules": "~0.14.5",
"react-native-web": "~0.13.12"
},
"devDependencies": {
"@babel/core": "^7.9.0",

View File

@ -0,0 +1,56 @@
import * as React from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';
import withWrap from './withWrap';
interface Props {
onPress?: () => void;
text?: string;
children?: any;
rippleColor?: string;
style?: any,
textStyle?: any
pressableStyle?: any,
}
export default withWrap(function Button(props: Props) {
return (
<View style={[ styles.button, props.style ]}>
<Pressable android_ripple={{ color: props.rippleColor ?? '#14181f' }}
style={[ styles.pressable, props.pressableStyle ]}
onPress={props.onPress}>
{props.text && <Text style={[ styles.text, props.textStyle ]}>{props.text}</Text>}
{props.children}
</Pressable>
</View>
);
});
const styles = StyleSheet.create({
button: {
flex: 1,
display: 'flex',
overflow: 'hidden',
minHeight: 52,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center'
},
pressable: {
flex: 1,
padding: 12,
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
},
text: {
fontSize: 16,
color: '#cde',
paddingBottom: 1
}
});

View File

@ -0,0 +1,142 @@
import * as React from 'react';
import Modal from 'react-native-modal';
import { View, StyleSheet, Pressable, FlatList } from 'react-native';
import Label from './Label';
import Button from './Button';
import withWrap from './withWrap';
import { ColorPicker } from 'react-native-color-picker';
interface Props {
label?: string;
value: string;
children?: any;
onValue?: (value: string) => void;
style?: any,
pressableStyle?: any,
switchStyle?: any;
labelStyle?: any
}
function hslToHex(h: number, s: number, l: number) {
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = (n: number) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0');
};
return `#${f(0)}${f(8)}${f(4)}`;
}
export default withWrap(function Select(props: Props) {
const [ selected, setSelected ] = React.useState<boolean>(false);
const [ tempColor, setTempColor ] = React.useState<string>(props.value);
const handlePick = () => {
setSelected(true);
setTempColor(props.value);
};
const handleChange = (color: any) => {
setTempColor(hslToHex(color.h, 100, 50));
};
const handleSet = () => {
props.onValue?.(tempColor);
setSelected(false);
};
return (
<React.Fragment>
<Button background={false} rippleColor='#345' onPress={handlePick}
style={props.style} pressableStyle={[ styles.color, props.pressableStyle, !props.label && styles.full ]}>
{props.label && <Label style={[ styles.label, props.labelStyle ]}>{props.label}</Label>}
{props.children}
<View style={[ styles.preview, !props.label && styles.fullColor, { backgroundColor: props.value } ]}/>
</Button>
<Modal
style={{ marginHorizontal: 64 }}
animationIn={animationIn}
animationInTiming={200}
animationOut={animationOut}
statusBarTranslucent={true}
isVisible={selected}
useNativeDriver={true}
useNativeDriverForBackdrop={true}
onBackButtonPress={() => setSelected(false)}
onBackdropPress={() => setSelected(false)}>
<View style={styles.modal}>
<ColorPicker style={styles.picker} hideSliders
oldColor={props.value} color={tempColor}
onOldColorSelected={() => setTempColor(props.value)}
onColorSelected={handleSet}
onColorChange={handleChange}/>
<Button text='Confirm' onPress={handleSet}
style={{width: '100%', flex: 0, backgroundColor: '#1c262f' }} rippleColor='#283344'/>
</View>
</Modal>
</React.Fragment>
);
}, true);
const styles = StyleSheet.create({
color: {
flexDirection: 'row',
justifyContent: 'space-between'
},
preview: {
width: 40,
height: 32,
marginRight: 8,
borderRadius: 4
},
label: {
textTransform: 'none',
letterSpacing: 0,
fontSize: 16,
marginTop: 0,
paddingLeft: 8
},
picker: {
width: '75%',
height: 240,
alignSelf: 'center',
},
modal: {
elevation: 5,
width: '100%',
color: 'white',
borderRadius: 8,
overflow: 'hidden',
paddingHorizontal: 12,
backgroundColor: '#181f24'
},
modalContainer: {
padding: 12
},
full: {
padding: 0
},
fullColor: {
flexGrow: 1,
height: 52,
marginRight: 0
}
});
const animationIn = {
from: { opacity: 0, transform: [{ scale: 0.85 }] },
to: { opacity: 1, transform: [{ scale: 1 }] }
};
const animationOut = {
from: animationIn.to,
to: animationIn.from
};

View File

@ -0,0 +1,23 @@
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
interface Props {
style?: any;
children?: any;
}
export default function Field(props: Props) {
return (
<View style={[ styles.field, props.style ]}>{props.children}</View>
);
}
const styles = StyleSheet.create({
field: {
marginTop: 8,
borderRadius: 8,
marginBottom: 16,
overflow: 'hidden',
backgroundColor: '#181f24'
}
});

View File

@ -0,0 +1,24 @@
import * as React from 'react';
import { Text, StyleSheet } from 'react-native';
interface Props {
text?: string;
children?: any;
style?: any;
}
export default function Label(props: Props) {
return (
<Text style={[ styles.label, props.style ]}>{props.text}{props.children}</Text>
);
}
const styles = StyleSheet.create({
label: {
fontSize: 12,
textTransform: 'uppercase',
letterSpacing: 1,
marginTop: 8,
color: '#abc'
}
});

View File

@ -0,0 +1,92 @@
import * as React from 'react';
import Modal from 'react-native-modal';
import { View, StyleSheet, Pressable, FlatList } from 'react-native';
import Button from './Button';
import withWrap from './withWrap';
interface Props {
value: string;
onSelect?: (value: string) => void;
data: { [key: string]: any, label: string, value: string }[];
}
export default withWrap(function Select(props: Props) {
const [ selected, setSelected ] = React.useState<boolean>(false);
const handleSelect = (value: string) => {
props.onSelect?.(value);
setSelected(false);
};
return (
<React.Fragment>
<Button background={false} rippleColor='#345'
text={props.data.filter(i => props.value === i.value)[0].label}
onPress={() => setSelected(true)}/>
<Modal
style={{ marginHorizontal: 32 }}
animationIn={animationIn}
animationInTiming={200}
animationOut={animationOut}
statusBarTranslucent={true}
isVisible={selected}
useNativeDriver={true}
useNativeDriverForBackdrop={true}
onBackButtonPress={() => setSelected(false)}
onBackdropPress={() => setSelected(false)}>
<View style={styles.modal}>
<FlatList
style={styles.modalScroll}
contentContainerStyle={styles.modalContainer}
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
data={props.data.map(d => ({ ...d, key: d.value }))}
renderItem={({ item }) => <Pressable key={item.value} style={styles.modalOption}>
<Button background={false} rippleColor='#123' text={item.label} onPress={() => handleSelect(item.value)}
textStyle={props.value === item.value && styles.modalOptionActiveText}
style={props.value === item.value && styles.modalOptionActive}/>
</Pressable>}/>
</View>
</Modal>
</React.Fragment>
);
});
const styles = StyleSheet.create({
modal: {
elevation: 5,
width: '100%',
color: 'white',
borderRadius: 8,
overflow: 'hidden',
backgroundColor: '#181f24'
},
modalScroll: {
maxHeight: 400
},
modalContainer: {
padding: 12
},
modalOption: {
width: '100%'
},
modalOptionActive: {
backgroundColor: '#1b262f'
},
modalOptionActiveText: {
color: '#fff',
fontWeight: 'bold'
}
});
const animationIn = {
from: { opacity: 0, transform: [{ scale: 0.85 }] },
to: { opacity: 1, transform: [{ scale: 1 }] }
};
const animationOut = {
from: animationIn.to,
to: animationIn.from
};

View File

@ -0,0 +1,74 @@
import * as React from 'react';
import { StyleSheet, Switch as DefaultSwitch } from 'react-native';
import Label from './Label';
import Button from './Button';
import withWrap from './withWrap';
interface SwitchColors { disabled: [ string, string ], enabled: [ string, string ] };
const DEFAULT_SWITCH_COLORS = {
disabled: [ '#123', '#789' ],
enabled: [ '#c46abd', '#ffc9fb' ]
};
interface Props {
value: boolean;
onValue?: () => void;
label?: any;
children?: any;
rippleColor?: string;
switchColor?: SwitchColors;
style?: any,
pressableStyle?: any,
switchStyle?: any;
labelStyle?: any
}
export default withWrap(function Switch(props: Props) {
const [ switchColors, setSwitchColors ] = React.useState<[ string, string ]>(
(props.switchColor ?? DEFAULT_SWITCH_COLORS)[props.value ? 'enabled' : 'disabled'] as any);
React.useEffect(() => {
const controller = new AbortController();
setTimeout(() => {
if (controller.signal.aborted) return;
setSwitchColors((props.switchColor ?? DEFAULT_SWITCH_COLORS)
[props.value ? 'enabled' : 'disabled'] as any);
}, 100);
return () => controller.abort();
}, [ props.value ]);
return (
<Button background={false} onPress={props.onValue}
pressableStyle={[ styles.switch, props.pressableStyle ]} rippleColor={props.rippleColor ?? '#345'}>
{props.label && <Label
style={[ styles.label, props.labelStyle ]}
>{props.label}</Label>}
{props.children}
<DefaultSwitch
trackColor={{ false: switchColors[0], true: switchColors[0] }}
thumbColor={switchColors[1]}
value={props.value}
onValueChange={props.onValue}/>
</Button>
);
}, true);
const styles = StyleSheet.create({
switch: {
flexDirection: 'row',
justifyContent: 'space-between'
},
label: {
textTransform: 'none',
letterSpacing: 0,
fontSize: 16,
marginTop: 0,
paddingLeft: 8
}
});

View File

@ -0,0 +1,6 @@
export { default as Button } from './Button';
export { default as Color } from './Color';
export { default as Field } from './Field';
export { default as Label } from './Label';
export { default as Select } from './Select';
export { default as Switch } from './Switch';

View File

@ -0,0 +1,22 @@
import Label from './Label';
import Field from './Field';
import React from 'react';
interface Props {
label?: string;
background?: boolean;
}
export default function withWrap<P>(Elem: React.FunctionComponent<P>, labelPassthrough: boolean = false) {
return function WithWrap(props: P & Props) {
return (
<React.Fragment>
{props.label && !labelPassthrough && <Label text={props.label}/>}
{(props.background ?? true)
? <Field><Elem {...props}/></Field>
: <Elem {...props}/>
}
</React.Fragment>
);
};
}

View File

@ -1,5 +1,7 @@
import * as React from 'react';
import { StyleSheet, View, Pressable, Text, ScrollView, Switch, FlatList } from 'react-native';
import { StyleSheet, View, Text, Switch, FlatList } from 'react-native';
import { Button } from '../field';
import * as Calendars from '../native/Calendars';
interface CalendarItemProps {
@ -30,16 +32,14 @@ function CalendarItem(props: CalendarItemProps) {
}, [ props.enabled ]);
return (
<View style={styles.item}>
<Pressable android_ripple={{ color: '#14181f' }} style={styles.itemPressable} onPress={props.onSwitch}>
<View style={[ styles.itemDot, { backgroundColor: props.color }]}/>
<Text style={[ styles.itemTitle, { color: props.enabled ? '#c0d8dd' : '#567' }]}>{props.title}</Text>
<Switch
trackColor={{ false: switchColors.background, true: switchColors.background }}
thumbColor={switchColors.foreground}
value={props.enabled} onValueChange={props.onSwitch}/>
</Pressable>
</View>
<Button background={false} onPress={props.onSwitch} pressableStyle={styles.item}>
<View style={[ styles.itemDot, { backgroundColor: props.color }]}/>
<Text style={[ styles.itemTitle, { color: props.enabled ? '#c0d8dd' : '#567' }]}>{props.title}</Text>
<Switch
trackColor={{ false: switchColors.background, true: switchColors.background }}
thumbColor={switchColors.foreground}
value={props.enabled} onValueChange={props.onSwitch}/>
</Button>
);
};
@ -90,16 +90,7 @@ const styles = StyleSheet.create({
backgroundColor: '#11161f'
},
item: {
borderRadius: 8,
overflow: 'hidden',
height: 52,
},
itemPressable: {
flexDirection: 'row',
alignItems: 'center',
paddingStart: 12,
paddingRight: 16,
flex: 1
flexDirection: 'row'
},
itemDot: {
width: 12,

View File

@ -1,11 +1,61 @@
import * as React from 'react';
import { StyleSheet, View, ScrollView, Text } from 'react-native';
import { Field, Label, Color, Select, Switch } from '../field';
const NOTCH_SELECT_DATA = [
{ label: 'None', value: 'none' },
{ label: 'Quarter', value: 'quarter' },
{ label: 'Hour', value: 'hour' },
{ label: 'Minute', value: 'minute' }
];
export default function ThemesScreen() {
const [ notches, setNotches ] = React.useState<string>('none');
const [ notchesAmbient, setNotchesAmbient ] = React.useState<string>('none');
const [ showEvents, setShowEvents ] = React.useState<boolean>(false);
const [ showMinutes, setShowMinutes ] = React.useState<boolean>(false);
const [ showGlow, setShowGlow ] = React.useState<boolean>(false);
const [ glowColorA, setGlowColorA ] = React.useState<string>('#006fff');
const [ glowColorB, setGlowColorB ] = React.useState<string>('#00aeff');
const [ glowColorC, setGlowColorC ] = React.useState<string>('#d500ff');
return (
<View style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollViewInner}>
<Text style={styles.title}>Themes coming soon :)</Text>
<View style={{ flexDirection: 'row' }}>
<View style={{ flex: 1, marginRight: 12 }}>
<Select label='Notches' value={notches} onSelect={setNotches} data={NOTCH_SELECT_DATA}/>
</View>
<View style={{ flex: 1 }}>
<Select label='Notches - Ambient Mode'
value={notchesAmbient} onSelect={setNotches} data={NOTCH_SELECT_DATA}/>
</View>
</View>
<Label text='Elements'/>
<Field>
<Switch pressableStyle={{ paddingVertical: 16 }} background={false} label='Show Events'
value={showEvents} onValue={() => setShowEvents(!showEvents)}/>
<Switch pressableStyle={{ paddingVertical: 16 }} background={false} label='Show Minutes Hand'
value={showMinutes} onValue={() => setShowMinutes(!showMinutes)}/>
</Field>
<Label text='Colors'/>
<Field>
<Color background={false} label='Accent Color'
value={glowColorA} onValue={setGlowColorA}/>
<Switch pressableStyle={{ paddingVertical: 16 }} background={false} label='Background Glow'
value={showGlow} onValue={() => setShowGlow(!showGlow)}/>
<View style={[{ paddingLeft: 20, paddingRight: 20, paddingTop: 8, paddingBottom: 12,
flexDirection: 'row', justifyContent: 'center' }, !showGlow && { opacity: 0.5 }]}
pointerEvents={showGlow ? 'auto' : 'none'}>
<Color style={{ marginRight: 32, height: 40, minHeight: 40, borderRadius: 4 }}
background={false} value={glowColorA} onValue={setGlowColorA}/>
<Color style={{ marginRight: 32, height: 40, minHeight: 40, borderRadius: 4 }}
background={false} value={glowColorB} onValue={setGlowColorB}/>
<Color style={{ minHeight: 40, height: 40, borderRadius: 4 }}
background={false} value={glowColorC} onValue={setGlowColorC}/>
</View>
</Field>
</ScrollView>
</View>
);
@ -19,13 +69,11 @@ const styles = StyleSheet.create({
backgroundColor: 'black'
},
scrollViewInner: {
padding: 12,
alignItems: 'center',
padding: 12
},
title: {
color: '#567',
fontSize: 20,
marginTop: 32,
// fontWeight: 'bold',
marginTop: 32
}
});

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,17 @@
<?xml version='1.0' encoding='UTF-8'?>
<widget xmlns:tizen='http://tizen.org/ns/widgets' xmlns='http://www.w3.org/ns/widgets' id='http://yourdomain/Focus' version='1.0.0' viewmodes='maximized'>
<widget xmlns:tizen='http://tizen.org/ns/widgets' xmlns='http://www.w3.org/ns/widgets' id='https://aurailus.com/Focus' version='1.0.0' viewmodes='maximized'>
<tizen:application id='kFVD8WK8ro.Focus' package='kFVD8WK8ro' required_version='2.3.1' ambient_support='enable'/>
<!-- <tizen:category name='http://tizen.org/category/wearable_clock'/> -->
<tizen:category name='http://tizen.org/category/wearable_clock'/>
<feature name='http://developer.samsung.com/tizen/feature/network.accessory_protocol'/>
<feature name='http://tizen.org/feature/screen.shape.circle'/>
<feature name='http://tizen.org/feature/screen.size.all'/>
<content src='index.html'/>
<icon src='icon.png'/>
<name>Focus</name>
<tizen:metadata key='AccessoryServicesLocation' value='res/accessoryservices.xml'/>
<tizen:metadata key='master_app_name' value='Focus Companion'/>
<tizen:metadata key='master_app_packagename' value='com.aurailus.focuscompanion'/>
<tizen:privilege name='http://tizen.org/privilege/alarm'/>
<tizen:setting background-support='disable' encryption='disable'/>
<tizen:profile name='wearable'/>
</widget>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,11 @@
<resources>
<application name='Focus'>
<serviceProfile id='/com/aurailus/focus/SyncAP' name='SyncAP' role='consumer' version='1.0'>
<supportedTransports>
<transport type='TRANSPORT_BT'/>
<transport type='TRANSPORT_WIFI'/>
</supportedTransports>
<serviceChannel id='110' dataRate='low' priority='low' reliability='enable'/>
</serviceProfile>
</application>
</resources>

1
watch/res/battery.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="#333" d="M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.67C6,21.4 6.6,22 7.33,22H16.67A1.33,1.33 0 0,0 18,20.67V5.33C18,4.6 17.4,4 16.67,4Z" /></svg>

After

Width:  |  Height:  |  Size: 440 B

5
watch/res/heart.svg Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24">
<path fill="#333" d="M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z" />
</svg>

After

Width:  |  Height:  |  Size: 498 B

1
watch/res/steps.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="#333" d="M10.74,11.72C11.21,12.95 11.16,14.23 9.75,14.74C6.85,15.81 6.2,13 6.16,12.86L10.74,11.72M5.71,10.91L10.03,9.84C9.84,8.79 10.13,7.74 10.13,6.5C10.13,4.82 8.8,1.53 6.68,2.06C4.26,2.66 3.91,5.35 4,6.65C4.12,7.95 5.64,10.73 5.71,10.91M17.85,19.85C17.82,20 17.16,22.8 14.26,21.74C12.86,21.22 12.8,19.94 13.27,18.71L17.85,19.85M20,13.65C20.1,12.35 19.76,9.65 17.33,9.05C15.22,8.5 13.89,11.81 13.89,13.5C13.89,14.73 14.17,15.78 14,16.83L18.3,17.9C18.38,17.72 19.89,14.94 20,13.65Z" /></svg>

After

Width:  |  Height:  |  Size: 781 B

View File

@ -13,15 +13,20 @@ const TITLE_SIZE = 16;
/** The color that displays behind event names. */
const EVENT_BACKGROUND_COLOR = 'rgba(65, 199, 232, 0.2)';
/** 90 degree turn in radians. */
const QUARTER_TURN = degToRad(90);
/**
* Handles drawing shapes used by the watch face.
* init() loads resources, and **must** be awaited before using any operations.
*/
export default class Artist {
readonly radius: number;
icons: Record<string, HTMLImageElement>;
readonly radius: number;
readonly ctx: CanvasRenderingContext2D;
private glowCtx: CanvasRenderingContext2D;
private glowImg: HTMLImageElement;
@ -34,6 +39,7 @@ export default class Artist {
constructor(canvas: HTMLCanvasElement | CanvasRenderingContext2D) {
if ('getContext' in canvas) this.ctx = canvas.getContext('2d')!;
else this.ctx = canvas;
this.icons = {};
this.radius = this.ctx.canvas.width / 2;
const glowCanvas = document.createElement('canvas');
@ -43,18 +49,30 @@ export default class Artist {
this.glowImg = null as any;
}
loadImage(path: string): Promise<HTMLImageElement> {
return new Promise<HTMLImageElement>(resolve => {
const img = document.createElement('img');
img.onload = () => resolve(img);
img.src = path;
});
}
/**
* Loads resources needed for certain draw operations.
*
* @returns a promise indicating that the loading is complete.
*/
init(): Promise<void> {
return new Promise<void>(resolve => {
this.glowImg = document.createElement('img');
this.glowImg.onload = () => resolve();
this.glowImg.src = '../res/glow.png';
});
async init(): Promise<void> {
const [ glowImg, battery, steps, heart ] = await Promise.all([
this.loadImage('../res/glow.png'),
this.loadImage('../res/battery.svg'),
this.loadImage('../res/steps.svg'),
this.loadImage('../res/heart.svg')
]);
this.glowImg = glowImg;
this.icons = { battery, steps, heart };
}
/**
@ -72,22 +90,33 @@ export default class Artist {
* @param radians - The angle in radians to draw the circle at, starting at the top and moving clockwise.
* @param dist - The distance from the center of the canvas to draw the circle at.
* @param radius - The radius of the circle to draw.
* @param color - The color to draw the circle with.
* @param fill - The color to fill the circle with. undefined will result in no fill.
* @param stroke - The color to trace the circle with. undefined will result in no stroke.
* @param strokeWidth - The width of the stroke for the circle with. undefined will result in no stroke.
*/
circle(radians: number, dist: number, radius: number, color: string) {
circle(radians: number, dist: number, radius: number, fill?: string, stroke?: string, strokeWidth?: number) {
const { ctx, radius: canvRadius } = this;
ctx.save();
ctx.translate(canvRadius, canvRadius);
ctx.rotate(radians);
ctx.rotate(radians - QUARTER_TURN);
ctx.beginPath();
ctx.arc(dist, 0, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
if (fill) {
ctx.fillStyle = fill;
ctx.fill();
}
if (stroke && strokeWidth) {
ctx.strokeStyle = stroke;
ctx.lineWidth = strokeWidth;
ctx.stroke();
}
ctx.restore();
}
@ -249,14 +278,15 @@ export default class Artist {
ctx.restore();
this.circle(startAngle, dist - EVENT_WIDTH / 2, EVENT_WIDTH / 2 - 4, event.color);
this.circle(startAngle + QUARTER_TURN,
dist - EVENT_WIDTH / 2, EVENT_WIDTH / 2 - 4, event.color);
ctx.font = `900 ${TITLE_SIZE}px Arial sans-serif`;
ctx.fillStyle = '#fff';
ctx.textBaseline = 'top';
ctx.textAlign = 'center';
let currentAngle = startAngle + degToRad(96);
let currentAngle = startAngle + QUARTER_TURN + degToRad(6);
for (let i = 0; i < event.title.length; i++) {
ctx.save();
ctx.translate(canvRadius, canvRadius);

View File

@ -0,0 +1,7 @@
export type ComplicationType = 'date' | 'battery' | 'steps' | 'weather' | 'heartrate';
export const ComplicationStyle = {
STANDALONE: 0,
OUTLINED: 1,
FILLED: 2
};

View File

@ -22,8 +22,8 @@ export function getEvents(): Event[] {
};
return [
{ start: createDate(9, 30), end: createDate(11, 20), color: '#59acff', title: 'PAAS' },
{ start: createDate(12, 30), end: createDate(1, 20), color: '#59acff', title: 'STAT' },
{ start: createDate(1, 30), end: createDate(2, 20), color: '#59acff', title: 'MATH' }
{ start: createDate(9, 30), end: createDate(11, 20), color: '#59acff', title: 'Japanese' },
{ start: createDate(12, 30), end: createDate(1, 20), color: '#59acff', title: 'Stats' },
{ start: createDate(1, 30), end: createDate(2, 20), color: '#59acff', title: 'Math' }
];
}

96
watch/src/Service.ts Normal file
View File

@ -0,0 +1,96 @@
// let SAAgent: any = null;
// let SASocket: any = null;
// let CHANNELID = 110;
// function onreceive(channelId: string, data: string) {
// alert('recv ' + channelId + ' ' + data);
// }
// function disconnect() {
// try {
// if (SASocket !== null) {
// SASocket.close();
// SASocket = null;
// alert('closed connection');
// }
// }
// catch(err: any) {
// console.log('exception [' + err.name + '] msg[' + err.message + ']');
// }
// }
// let agentCallback = {
// onconnect : function(socket: any) {
// SASocket = socket;
// alert('Successfully connected to remote peer.');
// SASocket.setSocketStatusListener(function(reason: string){
// alert('connection lost, Reason: [' + reason + ']');
// disconnect();
// });
// SASocket.setDataReceiveListener(onreceive);
// },
// onerror: alert
// };
// function connect() {
// alert('Goin');
// if (SASocket) {
// alert('already connected');
// return false;
// }
// try {
// // @ts-ignore
// webapis.sa.requestSAAgent(onsuccess, function (err: any) {
// alert('err [' + err.name + '] msg[' + err.message + ']');
// });
// }
// catch(err: any) {
// alert('exception [' + err.name + '] msg[' + err.message + ']');
// }
// return true;
// }
// function fetch() {
// try {
// SASocket.sendData(CHANNELID, 'Hello Accessory!');
// }
// catch(err: any) {
// alert('exception [' + err.name + '] msg[' + err.message + ']');
// }
// }
export default async function init() {
try {
// @ts-ignore - webapis is not defined.
const agents: any[] = await new Promise((resolve, reject) => webapis.sa.requestSAAgent(resolve,
(err: Error) => reject(new Error(`[1] ${err.name}: ${err.message}`))));
if (agents.length !== 1) throw new Error('Expected a single SAAgent, got ' + agents.length);
const agent = agents[0];
const peer = await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Finding peers timed out.')), 1000);
agent.setPeerAgentFindListener({
onpeeragentfound: function(peer: any) {
if (peer.appName === 'SyncAP') {
clearTimeout(timeout);
resolve(peer);
}
else reject(new Error(`Unexpected peer found: ${peer.appName}.`));
},
onerror: function(err: string) {
reject(new Error(`[2] ${err}`));
}
});
agent.findPeerAgents();
});
alert('gotcha ' + JSON.stringify(peer));
// SAAgent.setServiceConnectionListener(agentCallback);
// SAAgent.requestServiceConnection(peerAgent);
}
catch (e) {
if (e instanceof Error) alert('ERROR: ' + e.message);
else alert('UNHANDLED ERROR: ' + (e as any));
}
}

View File

@ -1,3 +1,4 @@
import { ComplicationType, ComplicationStyle } from './Complication';
/**
* Specifies how the notches should be rendered.
@ -5,7 +6,7 @@
export const NotchMode = {
NONE: 0,
QUARTER: 1,
QUARTERS: 1,
HOURS: 2,
MINUTES: 3
};
@ -16,10 +17,12 @@ export const NotchMode = {
export interface Theme {
notchMode: number;
notchModeAmbient: number;
showEvents: boolean;
showMinutes: boolean;
showGlow: boolean;
glowColors: [ string, string, string ];
complications: ({ type: ComplicationType; style: number } | null)[];
}
/**
@ -29,13 +32,34 @@ export interface Theme {
export function getTheme(): Theme {
return {
notchMode: NotchMode.MINUTES,
notchModeAmbient: NotchMode.HOURS,
showEvents: true,
showMinutes: true,
showGlow: true,
// glowColors: [
// 'rgba(66, 148, 255, 1)',
// 'rgba(5, 124, 242, 0.6)',
// 'rgba(198, 0, 237, 1)'
// ],
glowColors: [
'#4294ff',
'rgba(5, 124, 242, 0.6)',
'rgba(198, 0, 237, 1)'
]
'rgba(172, 77, 255, 1)',
'rgba(255, 54, 134, 0.4)',
'rgba(255, 71, 249, 0.6)'
],
// glowColors: [
// 'rgba(54, 255, 87)',
// 'rgba(0, 207, 155)',
// 'rgba(122, 255, 82, 0.6)'
// ],
complications: [ null, {
type: 'heartrate',
style: ComplicationStyle.OUTLINED
}, {
type: 'date',
style: ComplicationStyle.OUTLINED
}, {
type: 'battery',
style: ComplicationStyle.OUTLINED
}]
};
}

View File

@ -2,19 +2,21 @@ import Artist from './Artist';
import { degToRad } from './Util';
import { getEvents, Event } from './Events';
import { getTheme, NotchMode, Theme } from './Theme';
import { ComplicationType } from './Complication';
// import connect from './Service';
const NOTCH_MINUTE_COLOR = 'rgba(65, 199, 232, 0.15)';
const NOTCH_HOUR_COLOR = 'rgba(65, 199, 232, 0.33)';
const NOTCH_QUARTER_COLOR = 'rgba(65, 199, 232, 0.8)';
/** The minimum distance that all elements should be from the screen edge. */
const OUTER_BUFFER = 2;
const NOTCH_QUARTER_COLOR = 'rgba(65, 199, 232, 0.5)';
/** The distance that elements should be away from the screen edge in addition to OUTER_BUFFER if notches are drawn. */
const NOTCH_BUFFER = 12;
/** The text size of the day. */
const DAY_SIZE = 20;
/** The text size of the date. */
const DATE_SIZE = 20;
const DATE_SIZE = 24;
/**
* Handles app events, drawing using the artist, ambient mode, and watch <-> companion communication.
@ -35,6 +37,8 @@ export default class Watch {
this.theme = getTheme();
this.events = getEvents();
// connect();
/** Triggered only in ambient mode. */
window.addEventListener('timetick', () => this.draw());
@ -70,6 +74,96 @@ export default class Watch {
catch (e) { return new Date(); }
}
/**
*
* @param pos - The position of the complication, starting at 0 for the top and moving 90 degrees per number increase.
*/
private drawComplication(pos: number, type: ComplicationType, style: number) {
const { artist } = this;
const offsetScale = artist.radius / 2;
const angle = degToRad(pos * 90);
const x = Math.cos(angle - degToRad(90)) * offsetScale;
const y = Math.sin(angle - degToRad(90)) * offsetScale;
if (style > 0) artist.circle(angle, offsetScale, 40,
style === 2 ? (this.ambient ? '#aaa' : '#777') : undefined,
style === 1 ? (this.ambient ? '#777' : '#444') : undefined, 3);
artist.ctx.save();
artist.ctx.translate(artist.radius, artist.radius);
const alpha = (v: number, v2?: number) => style > 1
? `rgba(0, 0, 0, ${(style < 2 && v2 || v) * (this.ambient ? 2 : 1)})`
: `rgba(255, 255, 255, ${(style < 2 && v2 || v) * (this.ambient ? 2 : 1)})`;
switch (type) {
case 'date':
const time = this.getTime();
artist.ctx.font = `900 ${DAY_SIZE}px Arial sans-serif`;
artist.ctx.fillStyle = alpha(.9, .7);
artist.ctx.textBaseline = 'bottom';
artist.ctx.textAlign = 'center';
artist.ctx.fillText(`${['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'][time.getDay()]}`, x, y - 2);
artist.ctx.fillStyle = alpha(.6, .2);
artist.ctx.fillRect(x - DAY_SIZE, y - 1, 2 * DAY_SIZE, 3);
artist.ctx.fillStyle = alpha(.9, .7);
artist.ctx.font = `900 ${DATE_SIZE}px Arial sans-serif`;
artist.ctx.fillText(`${time.getDate()}`, x, y + 31);
break;
case 'battery':
const battery = 50;
artist.ctx.drawImage(artist.icons.battery, x - 18, y - 18 - 14, 36, 36);
artist.ctx.font = '900 24px Arial sans-serif';
artist.ctx.textBaseline = 'top';
artist.ctx.textAlign = 'center';
artist.ctx.fillStyle = alpha(.9, .7);
artist.ctx.fillText(battery.toString(), x, y + 4);
break;
case 'steps':
const steps = 1800;
artist.ctx.drawImage(artist.icons.steps, x - 18, y - 18 - 14, 36, 36);
artist.ctx.font = '900 22px Arial sans-serif';
artist.ctx.textBaseline = 'top';
artist.ctx.textAlign = 'center';
artist.ctx.fillStyle = alpha(.9, .7);
artist.ctx.fillText(steps.toString(), x, y + 4);
break;
case 'heartrate':
const heartrate = 85;
artist.ctx.drawImage(artist.icons.heart, x - 18, y - 18 - 14, 36, 36);
artist.ctx.font = '900 24px Arial sans-serif';
artist.ctx.textBaseline = 'top';
artist.ctx.textAlign = 'center';
artist.ctx.fillStyle = alpha(.9, .7);
artist.ctx.fillText(heartrate.toString(), x, y + 4);
break;
default:
break;
}
artist.ctx.restore();
}
private draw() {
const { artist, theme, events } = this;
@ -78,17 +172,11 @@ export default class Watch {
const time = this.getTime();
artist.ctx.font = `900 ${DATE_SIZE}px Arial sans-serif`;
artist.ctx.fillStyle = '#aaa';
artist.ctx.textBaseline = 'bottom';
artist.ctx.textAlign = 'center';
artist.ctx.fillText(`${['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'][time.getDay()]} ${time.getDate()}`,
artist.radius, artist.radius + artist.radius / 1.75);
artist.ctx.fillStyle = '#666';
artist.ctx.fillRect(artist.radius - 1 * DATE_SIZE,
artist.radius + artist.radius / 1.75 + 2, 2 * DATE_SIZE, 2);
for (let i = 0; i < 4; i++) {
let complication = theme.complications[i];
if (complication === null) continue;
this.drawComplication(i, complication.type, complication.style);
}
if (!this.ambient && theme.showGlow) {
this.animStep = (this.animStep + 1) % 360;
@ -105,26 +193,27 @@ export default class Watch {
artist.ctx.globalCompositeOperation = 'source-over';
}
if (theme.notchMode !== NotchMode.NONE) {
const notchMode = this.ambient ? theme.notchModeAmbient : theme.notchMode;
const outerBuffer = theme.notchModeAmbient === NotchMode.NONE ? 2 : 8;
if (notchMode !== NotchMode.NONE) {
for (let i = 0; i < 12 * 5; i++) {
if (i % 15 === 0) {
artist.circle(degToRad(i / (12 * 5) * 360),
artist.radius - OUTER_BUFFER - 4, 4, NOTCH_QUARTER_COLOR);
}
else if (i % 5 === 0 && theme.notchMode >= NotchMode.HOURS) {
if ((i % 5 === 0 && notchMode >= NotchMode.HOURS) || (i % 15 === 0 && notchMode >= NotchMode.QUARTERS)) {
artist.circle(degToRad(i / (12 * 5) * 360 - 90),
artist.radius - OUTER_BUFFER - 4, 3, NOTCH_HOUR_COLOR);
artist.radius - outerBuffer - 4, 3,
this.ambient ? NOTCH_QUARTER_COLOR : NOTCH_HOUR_COLOR);
}
else if (theme.notchMode >= NotchMode.MINUTES) {
else if (notchMode >= NotchMode.MINUTES) {
artist.circle(degToRad(i / (12 * 5) * 360 - 90),
artist.radius - OUTER_BUFFER - 4, 2, NOTCH_MINUTE_COLOR);
artist.radius - outerBuffer - 4, 2,
this.ambient ? NOTCH_HOUR_COLOR : NOTCH_MINUTE_COLOR);
}
}
}
if (theme.showEvents) {
for (let event of events) {
let eventBuffer = OUTER_BUFFER + theme.notchMode !== NotchMode.NONE ? NOTCH_BUFFER : 0;
let eventBuffer = outerBuffer + (theme.notchMode !== NotchMode.NONE ? NOTCH_BUFFER : 0);
artist.event(event, artist.radius - eventBuffer);
}
}
@ -134,11 +223,11 @@ export default class Watch {
if (theme.showMinutes) {
const minuteAngle = degToRad((time.getMinutes() + time.getSeconds() / 60) / 60 * 360 - 180);
artist.rounded(minuteAngle, 40, artist.radius - 64, 16, NOTCH_QUARTER_COLOR);
artist.rounded(minuteAngle, 38, artist.radius - 56, 12, NOTCH_QUARTER_COLOR);
}
const hourAngle = degToRad(((time.getHours() % 12) + time.getMinutes() / 60) / 12 * 360 - 180);
artist.rounded(hourAngle, 42, artist.radius - 92, 16,
artist.rounded(hourAngle, 42, artist.radius - 80, 16,
'rgba(0, 0, 0, 0.15)', '#fff', 4);
if (!this.ambient) this.animTimeout = setTimeout(() =>