feat: add item edit and delete features, add item logo (#4)
This commit is contained in:
parent
8de464cd9a
commit
3f927d14c6
26
Account.js
26
Account.js
|
@ -15,19 +15,22 @@
|
|||
import totp from "totp-generator";
|
||||
|
||||
class Account {
|
||||
constructor(description, secretCode, onUpdate) {
|
||||
constructor(description, secretCode, onUpdate, icon) {
|
||||
this.title = description;
|
||||
this.secretCode = secretCode;
|
||||
this.countdowns = 30;
|
||||
this.timer = setInterval(this.updateCountdown.bind(this), 1000);
|
||||
this.token = "";
|
||||
this.onUpdate = onUpdate;
|
||||
this.isEditing = false;
|
||||
this.icon = icon ? icon : null;
|
||||
}
|
||||
|
||||
generateToken() {
|
||||
if (this.secretCode !== null && this.secretCode !== undefined && this.secretCode !== "") {
|
||||
const token = totp(this.secretCode);
|
||||
return token;
|
||||
const tokenWithSpace = token.slice(0, 3) + " " + token.slice(3);
|
||||
return tokenWithSpace;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,6 +47,25 @@ class Account {
|
|||
}
|
||||
this.onUpdate();
|
||||
}
|
||||
getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
setTitle(title) {
|
||||
this.title = title;
|
||||
this.setEditingStatus(false);
|
||||
}
|
||||
setEditingStatus(status) {
|
||||
this.isEditing = status;
|
||||
this.onUpdate();
|
||||
}
|
||||
getEditStatus() {
|
||||
return this.isEditing;
|
||||
}
|
||||
|
||||
deleteAccount() {
|
||||
clearInterval(this.timer);
|
||||
this.onUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
export default Account;
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React, {useState} from "react";
|
||||
import {Text, TextInput, View} from "react-native";
|
||||
import {Button, IconButton} from "react-native-paper";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export default function EnterAccountDetails({onClose, onEdit, placeholder}) {
|
||||
EnterAccountDetails.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
const handleConfirm = () => {
|
||||
onEdit(description);
|
||||
};
|
||||
return (
|
||||
<View style={{flex: 1, justifyContent: "center", alignItems: "center"}}>
|
||||
<Text style={{fontSize: 24, marginBottom: 5}}>Enter new description</Text>
|
||||
<View style={{flexDirection: "row", alignItems: "center"}}>
|
||||
<IconButton icon="account-details" size={35} />
|
||||
<TextInput
|
||||
placeholder={placeholder}
|
||||
value={description}
|
||||
onChangeText={(text) => setDescription(text)}
|
||||
style={{borderWidth: 3, borderColor: "white", margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18, color: "gray", paddingLeft: 10}}
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
style={{
|
||||
backgroundColor: "#E6DFF3",
|
||||
borderRadius: 5,
|
||||
margin: 10,
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
top: 160,
|
||||
width: 300,
|
||||
}}
|
||||
onPress={handleConfirm}
|
||||
>
|
||||
<Text style={{fontSize: 18, width: 280}}>Confirm</Text>
|
||||
</Button>
|
||||
<IconButton icon={"close"} size={30} onPress={onClose} style={{position: "absolute", top: 5, right: 5}} />
|
||||
</View>
|
||||
);
|
||||
}
|
|
@ -16,9 +16,9 @@ import * as React from "react";
|
|||
import {Appbar, Avatar, Text} from "react-native-paper";
|
||||
|
||||
const Header = () => (
|
||||
<Appbar.Header>
|
||||
<Appbar.Header style={{height: 40}}>
|
||||
<Appbar.Content title="Casdoor" />
|
||||
<Avatar.Image size={32} style={{marginRight: 10, backgroundColor: "white"}} source={{uri: "https://cdn.casbin.com/casdoor/avatar/built-in/admin.jpeg"}} />
|
||||
<Avatar.Image size={32} source={{uri: "https://cdn.casbin.org/img/social_casdoor.png"}} style={{marginRight: 10, backgroundColor: "transparent"}} />
|
||||
<Text style={{marginRight: 10}} variant="titleMedium">Admin</Text>
|
||||
</Appbar.Header>
|
||||
);
|
||||
|
|
124
HomePage.js
124
HomePage.js
|
@ -14,12 +14,14 @@
|
|||
|
||||
import * as React from "react";
|
||||
import {Dimensions, FlatList, Text, TouchableOpacity, View} from "react-native";
|
||||
import {IconButton, List, Modal, Portal} from "react-native-paper";
|
||||
import {Avatar, Divider, IconButton, List, Modal, Portal} from "react-native-paper";
|
||||
import SearchBar from "./SearchBar";
|
||||
import {GestureHandlerRootView, Swipeable} from "react-native-gesture-handler";
|
||||
|
||||
import EnterAccountDetails from "./EnterAccountDetails";
|
||||
import Account from "./Account";
|
||||
import ScanQRCode from "./ScanQRCode";
|
||||
import EditAccountDetails from "./EditAccountDetails";
|
||||
|
||||
export default function HomePage() {
|
||||
const [isPlusButton, setIsPlusButton] = React.useState(true);
|
||||
|
@ -29,7 +31,12 @@ export default function HomePage() {
|
|||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
const [filteredData, setFilteredData] = React.useState(accountList);
|
||||
const [showScanner, setShowScanner] = React.useState(false);
|
||||
|
||||
const [showEditAccountModal, setShowEditAccountModal] = React.useState(false);
|
||||
const swipeableRef = React.useRef(null);
|
||||
const [placeholder, setPlaceholder] = React.useState("");
|
||||
const closeEditAccountModal = () => {
|
||||
setShowEditAccountModal(false);
|
||||
};
|
||||
const handleScanPress = () => {
|
||||
setShowScanner(true);
|
||||
setIsPlusButton(true);
|
||||
|
@ -60,11 +67,11 @@ export default function HomePage() {
|
|||
setShowEnterAccountModal(false);
|
||||
};
|
||||
|
||||
const onUpdate = () => {
|
||||
setAccountList(prevList => [...prevList]);
|
||||
};
|
||||
const handleAddAccount = (accountData) => {
|
||||
const onUpdate = () => {
|
||||
setAccountList(prevList => [...prevList]);
|
||||
};
|
||||
const newAccount = new Account(accountData.description, accountData.secretCode, onUpdate);
|
||||
const newAccount = new Account(accountData.description, accountData.secretCode, onUpdate, accountData.icon);
|
||||
const token = newAccount.generateToken();
|
||||
newAccount.token = token;
|
||||
|
||||
|
@ -72,6 +79,41 @@ export default function HomePage() {
|
|||
closeEnterAccountModal();
|
||||
};
|
||||
|
||||
const handleDeleteAccount = (accountDescp) => {
|
||||
const accountToDelete = accountList.find(account => {
|
||||
return account.getTitle() === accountDescp;
|
||||
});
|
||||
if (accountToDelete) {
|
||||
accountToDelete.deleteAccount();
|
||||
}
|
||||
setAccountList(prevList => prevList.filter(account => account.getTitle() !== accountDescp));
|
||||
};
|
||||
const handleEditAccount = (accountDescp) => {
|
||||
closeSwipeableMenu();
|
||||
setPlaceholder(accountDescp);
|
||||
setShowEditAccountModal(true);
|
||||
const accountToEdit = accountList.find(account => account.getTitle() === accountDescp);
|
||||
|
||||
if (accountToEdit) {
|
||||
accountToEdit.setEditingStatus(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onAccountEdit = (accountDescp) => {
|
||||
const accountToEdit = accountList.find(account => account.getEditStatus() === true);
|
||||
if (accountToEdit) {
|
||||
accountToEdit.setTitle(accountDescp);
|
||||
}
|
||||
setPlaceholder("");
|
||||
closeEditAccountModal();
|
||||
};
|
||||
|
||||
const closeSwipeableMenu = () => {
|
||||
if (swipeableRef.current) {
|
||||
swipeableRef.current.close();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (query) => {
|
||||
setSearchQuery(query);
|
||||
|
||||
|
@ -94,25 +136,50 @@ export default function HomePage() {
|
|||
<View style={{flex: 1}}>
|
||||
<SearchBar onSearch={handleSearch} />
|
||||
<FlatList
|
||||
// data={accountList}
|
||||
data={searchQuery.trim() !== "" ? filteredData : accountList}
|
||||
keyExtractor={(item, index) => index.toString()}
|
||||
renderItem={({item}) => (
|
||||
<List.Item
|
||||
title={
|
||||
<View>
|
||||
<Text style={{fontSize: 20}}>{item.title}</Text>
|
||||
<GestureHandlerRootView>
|
||||
<Swipeable
|
||||
ref={swipeableRef}
|
||||
renderRightActions={(progress, dragX) => (
|
||||
<View style={{flexDirection: "row", alignItems: "center"}}>
|
||||
<Text style={{fontSize: 40, width: 180}}>{item.token}</Text>
|
||||
<Text style={{fontSize: 20, width: 40}}>{item.countdowns}s</Text>
|
||||
<TouchableOpacity
|
||||
style={{height: 70, width: 80, backgroundColor: "#E6DFF3", alignItems: "center", justifyContent: "center"}}
|
||||
onPress={handleEditAccount.bind(this, item.title)}
|
||||
>
|
||||
<Text>Edit</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={{height: 70, width: 80, backgroundColor: "#FFC0CB", alignItems: "center", justifyContent: "center"}}
|
||||
onPress={handleDeleteAccount.bind(this, item.title)}
|
||||
>
|
||||
<Text>Delete</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
left={(props) => (
|
||||
<IconButton icon={"account"} size={70} style={{marginLeft: 20}} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<List.Item
|
||||
style={{height: 80, alignItems: "center", justifyContent: "center"}}
|
||||
title={
|
||||
<View>
|
||||
<Text style={{fontSize: 20}}>{item.title}</Text>
|
||||
<View style={{flexDirection: "row", alignItems: "center"}}>
|
||||
<Text style={{fontSize: 35, width: 180}}>{item.token}</Text>
|
||||
<Text style={{fontSize: 20, width: 40}}>{item.countdowns}s</Text>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
left={(props) => (
|
||||
item.icon ?
|
||||
<Avatar.Image size={60} source={{uri: item.icon}} style={{marginLeft: 20, marginRight: 20, borderRadius: 10, backgroundColor: "transparent"}} />
|
||||
: <Avatar.Icon size={80} icon={"account"} color={"black"} style={{marginLeft: 10, marginRight: 10, borderRadius: 10, backgroundColor: "transparent"}} />
|
||||
)}
|
||||
/>
|
||||
</Swipeable>
|
||||
</GestureHandlerRootView>
|
||||
)}
|
||||
ItemSeparatorComponent={() => <Divider />}
|
||||
/>
|
||||
|
||||
<Portal>
|
||||
|
@ -166,6 +233,25 @@ export default function HomePage() {
|
|||
<EnterAccountDetails onClose={closeEnterAccountModal} onAdd={handleAddAccount} />
|
||||
</Modal>
|
||||
</Portal>
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={showEditAccountModal}
|
||||
onDismiss={closeEditAccountModal}
|
||||
contentContainerStyle={{
|
||||
backgroundColor: "white",
|
||||
padding: 1,
|
||||
borderRadius: 10,
|
||||
width: "90%",
|
||||
height: "30%",
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: [{translateX: -offsetX}, {translateY: -offsetY}],
|
||||
}}
|
||||
>
|
||||
<EditAccountDetails onClose={closeEditAccountModal} onEdit={onAccountEdit} placeholder={placeholder} />
|
||||
</Modal>
|
||||
</Portal>
|
||||
{showScanner && (
|
||||
<ScanQRCode onClose={handleCloseScanner} showScanner={showScanner} onAdd={handleAddAccount} />
|
||||
)}
|
||||
|
|
|
@ -31,6 +31,7 @@ export default function NavigationBar() {
|
|||
}}
|
||||
tabBar={({navigation, state, descriptors, insets}) => (
|
||||
<BottomNavigation.Bar
|
||||
style={{height: 80}}
|
||||
navigationState={state}
|
||||
safeAreaInsets={insets}
|
||||
onTabPress={({route, preventDefault}) => {
|
||||
|
|
|
@ -42,12 +42,11 @@ const ScanQRCode = ({onClose, showScanner, onAdd}) => {
|
|||
// type org.iso.QRCode
|
||||
// data otpauth://totp/casdoor:built-in/admin?algorithm=SHA1&digits=6&issuer=casdoor&period=30&secret=DL5XI33M772GSGU73GJPCOIBNJE7TG3J
|
||||
// console.log(`Bar code with type ${type} and data ${data} has been scanned!`);
|
||||
|
||||
const description = data.match(/casdoor:([^?]+)/); // description casdoor:built-in/admin
|
||||
const description = data.match(/otpauth:\/\/totp\/([^?]+)/); // description casdoor:built-in/admin
|
||||
const secretCode = data.match(/secret=([^&]+)/); // secretCode II5UO7HIA3SPVXAB6KPAIXZ33AQP7C3R
|
||||
|
||||
const icon = data.match(/issuer=([^&]+)/);
|
||||
if (description && secretCode) {
|
||||
onAdd({description: description[1], secretCode: secretCode[1]});
|
||||
onAdd({description: description[1], secretCode: secretCode[1], icon: `https://cdn.casbin.org/img/social_${icon && icon[1].toLowerCase()}.png`});
|
||||
}
|
||||
|
||||
closeOptions();
|
||||
|
|
|
@ -27,6 +27,8 @@ const SearchBar = ({onSearch}) => {
|
|||
placeholder="Search"
|
||||
onChangeText={onChangeSearch}
|
||||
value={searchQuery}
|
||||
style={{height: 48, backgroundColor: "#E6DFF3"}}
|
||||
inputStyle={{textAlignVertical: "center", justifyContent: "center", alignItems: "center"}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.72.4",
|
||||
"react-native-gesture-handler": "^2.12.1",
|
||||
"react-native-paper": "^5.10.3",
|
||||
"react-native-web": "~0.19.6",
|
||||
"totp-generator": "^0.0.14"
|
||||
|
@ -2085,6 +2086,17 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@egjs/hammerjs": {
|
||||
"version": "2.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
|
||||
"integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
|
||||
"dependencies": {
|
||||
"@types/hammerjs": "^2.0.36"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
|
||||
|
@ -6467,6 +6479,11 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hammerjs": {
|
||||
"version": "2.0.41",
|
||||
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
|
||||
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
|
||||
},
|
||||
"node_modules/@types/html-minifier-terser": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
|
||||
|
@ -17041,6 +17058,22 @@
|
|||
"react": "18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-gesture-handler": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.12.1.tgz",
|
||||
"integrity": "sha512-deqh36bw82CFUV9EC4tTo2PP1i9HfCOORGS3Zmv71UYhEZEHkzZv18IZNPB+2Awzj45vLIidZxGYGFxHlDSQ5A==",
|
||||
"dependencies": {
|
||||
"@egjs/hammerjs": "^2.0.17",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"invariant": "^2.2.4",
|
||||
"lodash": "^4.17.21",
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-paper": {
|
||||
"version": "5.10.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.10.3.tgz",
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.72.4",
|
||||
"react-native-gesture-handler": "^2.12.1",
|
||||
"react-native-paper": "^5.10.3",
|
||||
"react-native-web": "~0.19.6",
|
||||
"totp-generator": "^0.0.14"
|
||||
|
|
Loading…
Reference in New Issue