feat: add item edit and delete features, add item logo (#4)

This commit is contained in:
ChenWenpeng 2023-09-18 22:40:56 +08:00 committed by GitHub
parent 8de464cd9a
commit 3f927d14c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 232 additions and 27 deletions

View File

@ -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;

61
EditAccountDetails.js Normal file
View File

@ -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>
);
}

View File

@ -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>
);

View File

@ -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} />
)}

View File

@ -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}) => {

View File

@ -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();

View File

@ -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"}}
/>
);
};

33
package-lock.json generated
View File

@ -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",

View File

@ -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"