Companion theme tab, Watch Complications WIP
|
@ -1,2 +1,3 @@
|
|||
node_modules
|
||||
watch/Focus.wgt
|
||||
watch/build
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 87 KiB |
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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
|
||||
};
|
|
@ -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'
|
||||
}
|
||||
});
|
|
@ -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'
|
||||
}
|
||||
});
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
|
BIN
watch/Focus.wgt
|
@ -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>
|
||||
|
|
BIN
watch/icon.png
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 27 KiB |
|
@ -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>
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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);
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export type ComplicationType = 'date' | 'battery' | 'steps' | 'weather' | 'heartrate';
|
||||
|
||||
export const ComplicationStyle = {
|
||||
STANDALONE: 0,
|
||||
OUTLINED: 1,
|
||||
FILLED: 2
|
||||
};
|
|
@ -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' }
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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(() =>
|
||||
|
|