You've already forked SmartisanNote.Remake
初始化提交
This commit is contained in:
18
src/App.vue
Normal file
18
src/App.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<AppDataProvider>
|
||||
<div>
|
||||
<router-view />
|
||||
</div>
|
||||
</AppDataProvider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { AppDataProvider } from './utils/AppDataContext';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
AppDataProvider
|
||||
}
|
||||
}
|
||||
</script>
|
||||
229
src/components/FolderItem.vue
Normal file
229
src/components/FolderItem.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<div
|
||||
@click="onPress"
|
||||
class="folder-item-container"
|
||||
:class="{ 'folder-item-selected': isSelected, 'folder-item-pressed': isPressed }"
|
||||
@mousedown="handleMouseDown"
|
||||
@mouseup="handleMouseUp"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<div class="folder-item-content">
|
||||
<!-- Hidden folder icon (matches native implementation) -->
|
||||
<img
|
||||
v-if="folderIconSrc"
|
||||
:src="folderIconSrc"
|
||||
class="folder-item-icon"
|
||||
alt="folder icon"
|
||||
style="visibility: hidden;"
|
||||
/>
|
||||
<ion-icon
|
||||
v-else
|
||||
:icon="folderIcon"
|
||||
class="folder-item-icon"
|
||||
style="visibility: hidden;"
|
||||
></ion-icon>
|
||||
|
||||
<!-- Checkbox for selection (visible when isSelected is true) -->
|
||||
<img
|
||||
v-if="isSelected"
|
||||
src="/assets/icons/drawable-xxhdpi/icon_folder_checked.png"
|
||||
class="folder-item-checkbox"
|
||||
alt="selected"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
src="/assets/icons/drawable-xxhdpi/icon_folder_unchecked.png"
|
||||
class="folder-item-checkbox"
|
||||
alt="not selected"
|
||||
/>
|
||||
|
||||
<div class="folder-item-text-container">
|
||||
<div class="folder-item-name">
|
||||
{{ name }}
|
||||
</div>
|
||||
<div class="folder-item-count">
|
||||
{{ noteCount }} 条便签
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected indicator (visible when isSelected is true) -->
|
||||
<img
|
||||
v-if="isSelected"
|
||||
src="/assets/icons/drawable-xxhdpi/folder_selected.png"
|
||||
class="folder-item-selected-icon"
|
||||
alt="selected"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { folder, star, trash, document } from 'ionicons/icons';
|
||||
|
||||
export default {
|
||||
name: 'FolderItem',
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
noteCount: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
onPress: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
folder,
|
||||
star,
|
||||
trash,
|
||||
document,
|
||||
isPressed: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
folderIcon() {
|
||||
switch (this.id) {
|
||||
case 'all':
|
||||
return this.folder;
|
||||
case 'starred':
|
||||
return this.star;
|
||||
case 'trash':
|
||||
return this.trash;
|
||||
default:
|
||||
return this.document;
|
||||
}
|
||||
},
|
||||
folderIconSrc() {
|
||||
switch (this.id) {
|
||||
case 'all':
|
||||
return '/assets/icons/drawable-xxhdpi/sidebar_folder_icon_all.png';
|
||||
case 'starred':
|
||||
return '/assets/icons/drawable-xxhdpi/sidebar_folder_icon_favorite.png';
|
||||
case 'trash':
|
||||
return '/assets/icons/drawable-xxhdpi/sidebar_folder_icon_trash.png';
|
||||
default:
|
||||
return '/assets/icons/drawable-xxhdpi/sidebar_folder_icon_document.png';
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleMouseDown() {
|
||||
this.isPressed = true;
|
||||
},
|
||||
handleMouseUp() {
|
||||
this.isPressed = false;
|
||||
},
|
||||
handleMouseLeave() {
|
||||
this.isPressed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.folder-item-container {
|
||||
position: relative;
|
||||
min-height: 44px;
|
||||
background-color: var(--background-card);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.folder-item-container.folder-item-selected {
|
||||
background-color: var(--folder-item-selected);
|
||||
}
|
||||
|
||||
.folder-item-container.folder-item-pressed {
|
||||
background-color: var(--black-05);
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.folder-item-container.folder-item-pressed::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('/assets/icons/drawable-xxhdpi/folder_item_pressed_bg.png') repeat;
|
||||
background-size: 100% 100%;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.folder-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 0 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.folder-item-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
color: var(--folder-name);
|
||||
flex-shrink: 0;
|
||||
margin-left: 1px;
|
||||
padding-left: 11px;
|
||||
}
|
||||
|
||||
.folder-item-icon[src] {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.folder-item-checkbox {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.folder-item-selected-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.folder-item-text-container {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.folder-item-name {
|
||||
font-size: 15px;
|
||||
font-weight: normal;
|
||||
color: var(--folder-name);
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.folder-item-count {
|
||||
font-size: 12px;
|
||||
color: var(--folder-count);
|
||||
line-height: 1.2;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
313
src/components/Header.vue
Normal file
313
src/components/Header.vue
Normal file
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<!-- 左侧区域 -->
|
||||
<div class="header-left-area">
|
||||
<!-- 左侧图标 -->
|
||||
<div
|
||||
v-if="onBack"
|
||||
@click="onBack"
|
||||
class="header-icon-button"
|
||||
>
|
||||
<img
|
||||
src="/assets/icons/drawable-xxhdpi/btn_back.png"
|
||||
class="header-icon"
|
||||
alt="back"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="leftIcon === 'back' && leftIconSource && onLeftAction"
|
||||
@click="onLeftAction"
|
||||
class="header-icon-button"
|
||||
>
|
||||
<img
|
||||
:src="leftIconSource"
|
||||
class="header-icon"
|
||||
alt="left icon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="leftIcon === 'settings'"
|
||||
@click="onLeftAction"
|
||||
class="header-icon-button"
|
||||
>
|
||||
<img
|
||||
:src="leftIconSource"
|
||||
class="header-icon"
|
||||
alt="settings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间标题区域 -->
|
||||
<div
|
||||
@click="onTitlePress"
|
||||
class="header-title-container"
|
||||
>
|
||||
<span class="header-title">
|
||||
{{ title }}
|
||||
</span>
|
||||
<!-- 文件夹切换按钮 (在标题区域内) -->
|
||||
<div
|
||||
v-if="leftIcon === 'settings'"
|
||||
@click.stop="handleFolderToggle"
|
||||
class="folder-toggle-button"
|
||||
>
|
||||
<img
|
||||
:src="folderExpanded ? '/assets/icons/drawable-xxhdpi/folder_title_arrow_pressed.png' : '/assets/icons/drawable-xxhdpi/folder_title_arrow_normal.png'"
|
||||
class="folder-toggle-icon"
|
||||
alt="folder toggle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧区域 -->
|
||||
<div class="header-right-area">
|
||||
<div
|
||||
v-if="onAction"
|
||||
@click="onAction"
|
||||
class="header-icon-button"
|
||||
>
|
||||
<img
|
||||
v-if="actionIconSource"
|
||||
:src="actionIconSource"
|
||||
class="header-icon"
|
||||
alt="action"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="header-action-text"
|
||||
>
|
||||
{{ actionText }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Header',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
onBack: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onAction: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
actionText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
actionIcon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
leftIcon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
onLeftAction: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onFolderToggle: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
isFolderExpanded: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
onTitlePress: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localFolderExpanded: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
folderExpanded() {
|
||||
// 优先使用父组件传递的isFolderExpanded状态,否则使用本地状态
|
||||
return this.isFolderExpanded !== undefined ? this.isFolderExpanded : this.localFolderExpanded;
|
||||
},
|
||||
actionIconSource() {
|
||||
// 根据actionIcon属性返回对应的图标路径
|
||||
switch (this.actionIcon) {
|
||||
case 'settings':
|
||||
return '/assets/icons/drawable-xxhdpi/btn_settings.png';
|
||||
case 'create':
|
||||
return '/assets/icons/drawable-xxhdpi/btn_create.png';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
leftIconSource() {
|
||||
// 根据leftIcon属性返回对应的图标路径
|
||||
switch (this.leftIcon) {
|
||||
case 'folder':
|
||||
// 文件夹图标根据展开状态切换
|
||||
return this.folderExpanded
|
||||
? '/assets/icons/drawable-xxhdpi/folder_title_arrow_pressed.png'
|
||||
: '/assets/icons/drawable-xxhdpi/folder_title_arrow_normal.png';
|
||||
case 'back':
|
||||
// 返回图标
|
||||
return '/assets/icons/drawable-xxhdpi/btn_back.png';
|
||||
case 'settings':
|
||||
// 设置图标
|
||||
return '/assets/icons/drawable-xxhdpi/btn_settings.png';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleFolderToggle() {
|
||||
// 切换文件夹展开状态
|
||||
if (this.isFolderExpanded === undefined) {
|
||||
// 如果父组件没有传递isFolderExpanded,则使用本地状态
|
||||
this.localFolderExpanded = !this.localFolderExpanded;
|
||||
}
|
||||
|
||||
// 调用父组件传递的回调函数
|
||||
if (this.onFolderToggle) {
|
||||
this.onFolderToggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background-color: var(--background-card);
|
||||
box-shadow: 0 1px 0 0 var(--border);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.header-left-area {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-right-area {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-icon-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-icon-button:hover {
|
||||
background-color: var(--black-05);
|
||||
}
|
||||
|
||||
.header-icon-button:active {
|
||||
background-color: var(--black-10);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.header-title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
flex: 1;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.header-title-container:hover {
|
||||
background-color: var(--black-05);
|
||||
}
|
||||
|
||||
.header-title-container:active {
|
||||
background-color: var(--black-10);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.folder-toggle-button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.folder-toggle-button:hover {
|
||||
background-color: var(--black-05);
|
||||
}
|
||||
|
||||
.folder-toggle-button:active {
|
||||
background-color: var(--black-10);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.folder-toggle-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.header-action-text {
|
||||
font-size: 16px;
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
223
src/components/NoteItem.vue
Normal file
223
src/components/NoteItem.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div class="note-item-container">
|
||||
<!-- 左滑删除操作区域 -->
|
||||
<div
|
||||
v-if="onDelete"
|
||||
class="delete-area"
|
||||
:class="{ 'delete-area-visible': isSwiped }"
|
||||
>
|
||||
<span class="delete-text">删除</span>
|
||||
</div>
|
||||
|
||||
<!-- 便签内容区域 -->
|
||||
<div
|
||||
class="note-content"
|
||||
:class="{
|
||||
'note-content-swiped': isSwiped,
|
||||
'note-content-pressed': isPressed
|
||||
}"
|
||||
@click="onPress"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
@mousedown="handleMouseDown"
|
||||
@mouseup="handleMouseUp"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<div class="note-header">
|
||||
<span class="note-date">
|
||||
{{ date }}
|
||||
</span>
|
||||
<ion-icon
|
||||
v-if="isStarred"
|
||||
:icon="star"
|
||||
class="star-icon"
|
||||
></ion-icon>
|
||||
</div>
|
||||
<div class="note-title-container">
|
||||
<span class="note-title">
|
||||
{{ firstLine || '无内容' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { star } from 'ionicons/icons';
|
||||
|
||||
export default {
|
||||
name: 'NoteItem',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
date: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isStarred: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
onPress: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
onDelete: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
star,
|
||||
isSwiped: false,
|
||||
isPressed: false,
|
||||
touchStartX: 0,
|
||||
touchEndX: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
firstLine() {
|
||||
if (!this.content) return '';
|
||||
return this.content.split('\n')[0];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTouchStart(event) {
|
||||
this.touchStartX = event.touches[0].clientX;
|
||||
this.isPressed = true;
|
||||
},
|
||||
handleTouchMove(event) {
|
||||
this.touchEndX = event.touches[0].clientX;
|
||||
// 如果有滑动,取消按下状态
|
||||
if (Math.abs(this.touchStartX - this.touchEndX) > 5) {
|
||||
this.isPressed = false;
|
||||
}
|
||||
},
|
||||
handleTouchEnd() {
|
||||
// 重置按下状态
|
||||
this.isPressed = false;
|
||||
|
||||
// 计算滑动距离
|
||||
const swipeDistance = this.touchStartX - this.touchEndX;
|
||||
|
||||
// 如果滑动距离超过阈值,则显示删除按钮
|
||||
if (swipeDistance > 50) {
|
||||
this.isSwiped = true;
|
||||
} else if (swipeDistance < -50) {
|
||||
this.isSwiped = false;
|
||||
}
|
||||
},
|
||||
handleMouseDown() {
|
||||
this.isPressed = true;
|
||||
},
|
||||
handleMouseUp() {
|
||||
this.isPressed = false;
|
||||
},
|
||||
handleMouseLeave() {
|
||||
this.isPressed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.note-item-container {
|
||||
height: 78.33px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1px;
|
||||
position: relative;
|
||||
background: var(--background-card);
|
||||
}
|
||||
|
||||
.delete-area {
|
||||
position: absolute;
|
||||
right: -80px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 80px;
|
||||
height: 100%;
|
||||
background: #ffe65c53;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: right 0.3s ease;
|
||||
}
|
||||
|
||||
.delete-area-visible {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.delete-text {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.note-content {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
background: var(--background-card);
|
||||
box-sizing: border-box;
|
||||
transition: transform 0.3s ease, background-color 0.2s ease;
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: relative;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.note-content-swiped {
|
||||
transform: translateX(-80px);
|
||||
}
|
||||
|
||||
.note-content-pressed {
|
||||
background: var(--background-secondary);
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.note-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 5px 12px 0 52px;
|
||||
position: relative;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.note-date {
|
||||
font-size: 10px;
|
||||
color: var(--note-date);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.star-icon {
|
||||
font-size: 16px;
|
||||
color: var(--note-star);
|
||||
}
|
||||
|
||||
.note-title-container {
|
||||
position: absolute;
|
||||
top: 38.5px;
|
||||
left: 52px;
|
||||
right: 12px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.note-title {
|
||||
font-size: 16.5px;
|
||||
color: var(--note-title);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 30px;
|
||||
}
|
||||
</style>
|
||||
31
src/main.js
Normal file
31
src/main.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import App from './App.vue'
|
||||
|
||||
// Pages
|
||||
import NoteListPage from './pages/NoteListPage.vue'
|
||||
import NoteDetailPage from './pages/NoteDetailPage.vue'
|
||||
import NoteEditorPage from './pages/NoteEditorPage.vue'
|
||||
import FolderPage from './pages/FolderPage.vue'
|
||||
import SettingsPage from './pages/SettingsPage.vue'
|
||||
|
||||
// Router
|
||||
const routes = [
|
||||
{ path: '/', redirect: '/notes' },
|
||||
{ path: '/notes', component: NoteListPage },
|
||||
{ path: '/notes/:id', component: NoteDetailPage, props: true },
|
||||
{ path: '/editor', component: NoteEditorPage },
|
||||
{ path: '/editor/:id', component: NoteEditorPage, props: true },
|
||||
{ path: '/folders', component: FolderPage },
|
||||
{ path: '/settings', component: SettingsPage }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// App
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
132
src/pages/FolderPage.vue
Normal file
132
src/pages/FolderPage.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<ion-page>
|
||||
<Header
|
||||
title="文件夹"
|
||||
:onBack="() => window.history.back()"
|
||||
/>
|
||||
<div style="padding: 10px; background-color: var(--background)">
|
||||
<div style="display: flex; align-items: center; background-color: var(--search-bar-background); border-radius: 8px; padding: 0 10px">
|
||||
<ion-icon :icon="search" style="font-size: 20px; color: var(--text-tertiary)"></ion-icon>
|
||||
<ion-input
|
||||
placeholder="搜索文件夹..."
|
||||
:value="searchQuery"
|
||||
@ionChange="e => setSearchQuery(e.detail.value)"
|
||||
style="--padding-start: 10px; --padding-end: 10px; flex: 1; font-size: 16px; color: var(--text-primary)"
|
||||
></ion-input>
|
||||
<ion-button
|
||||
v-if="searchQuery.length > 0"
|
||||
fill="clear"
|
||||
@click="() => setSearchQuery('')"
|
||||
>
|
||||
<ion-icon :icon="closeCircle" style="font-size: 20px; color: var(--text-tertiary)"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
<ion-content>
|
||||
<ion-list style="background-color: var(--background); padding: 0 16px; --ion-item-background: var(--background)">
|
||||
<FolderItem
|
||||
v-for="folder in filteredFolders"
|
||||
:key="folder.id"
|
||||
:id="folder.id"
|
||||
:name="folder.name"
|
||||
:noteCount="folder.noteCount"
|
||||
:onPress="() => handleFolderPress(folder.id)"
|
||||
:isSelected="folder.id === selectedFolder"
|
||||
/>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAppData } from '../utils/AppDataContext';
|
||||
import { search, closeCircle } from 'ionicons/icons';
|
||||
import FolderItem from '../components/FolderItem.vue';
|
||||
import Header from '../components/Header.vue';
|
||||
|
||||
export default {
|
||||
name: 'FolderPage',
|
||||
components: {
|
||||
FolderItem,
|
||||
Header
|
||||
},
|
||||
setup() {
|
||||
const { state } = useAppData();
|
||||
const searchQuery = ref('');
|
||||
const selectedFolder = ref('all');
|
||||
|
||||
// Calculate note count for each folder
|
||||
const foldersWithCount = computed(() => {
|
||||
return state.folders.map(folder => {
|
||||
const noteCount = state.notes.filter(note => note.folderId === folder.id).length;
|
||||
return {
|
||||
...folder,
|
||||
noteCount,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Add default folders at the beginning
|
||||
const allNotesCount = computed(() => state.notes.length);
|
||||
const starredNotesCount = computed(() => state.notes.filter(note => note.isStarred).length);
|
||||
// Assuming we have a way to track deleted notes in the future
|
||||
const trashNotesCount = 0;
|
||||
|
||||
const foldersWithAllNotes = computed(() => {
|
||||
return [
|
||||
{ id: 'all', name: '全部便签', noteCount: allNotesCount.value, createdAt: new Date() },
|
||||
{ id: 'starred', name: '加星便签', noteCount: starredNotesCount.value, createdAt: new Date() },
|
||||
{ id: 'trash', name: '回收站', noteCount: trashNotesCount, createdAt: new Date() },
|
||||
...foldersWithCount.value,
|
||||
];
|
||||
});
|
||||
|
||||
const handleFolderPress = (folderId) => {
|
||||
// 更新选中的文件夹状态
|
||||
selectedFolder.value = folderId;
|
||||
// 在实际应用中,这里会将选中的文件夹传递回NoteListScreen
|
||||
// 通过导航参数传递选中的文件夹ID
|
||||
window.location.hash = `#/notes?folder=${folderId}`;
|
||||
};
|
||||
|
||||
const handleAddFolder = () => {
|
||||
// In a full implementation, this would open a folder creation dialog
|
||||
console.log('Add folder pressed');
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
// In a full implementation, this would filter folders based on searchQuery
|
||||
console.log('Search for:', searchQuery.value);
|
||||
};
|
||||
|
||||
const handleBackPress = () => {
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
// Filter folders based on search query
|
||||
const filteredFolders = computed(() => {
|
||||
return foldersWithAllNotes.value.filter(folder =>
|
||||
folder.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
const setSearchQuery = (value) => {
|
||||
searchQuery.value = value;
|
||||
};
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
selectedFolder,
|
||||
filteredFolders,
|
||||
handleFolderPress,
|
||||
handleAddFolder,
|
||||
handleSearch,
|
||||
handleBackPress,
|
||||
setSearchQuery,
|
||||
search,
|
||||
closeCircle
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
139
src/pages/NoteDetailPage.vue
Normal file
139
src/pages/NoteDetailPage.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<ion-page>
|
||||
<Header
|
||||
:title="note ? note.title : '未找到便签'"
|
||||
:onBack="() => window.history.back()"
|
||||
:onAction="note ? handleEdit : null"
|
||||
actionIcon="settings"
|
||||
/>
|
||||
<ion-content v-if="!note">
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%; background-color: var(--background)">
|
||||
<ion-text>未找到该便签</ion-text>
|
||||
</div>
|
||||
</ion-content>
|
||||
<ion-content v-else>
|
||||
<div style="padding: 16px; background-color: var(--background-card)">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid var(--border)">
|
||||
<ion-text style="color: var(--note-date); font-size: 14px">
|
||||
最后更新: {{ formatDate(note.updatedAt) }}
|
||||
</ion-text>
|
||||
<ion-button fill="clear" @click="handleStar">
|
||||
<ion-icon
|
||||
:icon="note.isStarred ? star : starOutline"
|
||||
:style="{ fontSize: '24px', color: note.isStarred ? 'var(--note-star)' : 'var(--text-tertiary)' }"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
<div style="white-space: pre-wrap; line-height: 1.6; font-size: 16px; color: var(--note-content)">
|
||||
{{ note.content }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: space-around; padding: 10px; background-color: var(--background); border-top: 1px solid var(--border)">
|
||||
<div style="display: flex; flex-direction: column; align-items: center">
|
||||
<ion-button fill="clear" @click="handleShare">
|
||||
<ion-icon :icon="share" style="font-size: 24px; color: var(--text-primary)"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-text style="font-size: 12px; color: var(--text-primary)">分享</ion-text>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; align-items: center">
|
||||
<ion-button fill="clear" @click="handleMoveToFolder">
|
||||
<ion-icon :icon="folder" style="font-size: 24px; color: var(--text-primary)"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-text style="font-size: 12px; color: var(--text-primary)">文件夹</ion-text>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; align-items: center">
|
||||
<ion-button fill="clear" @click="handleReminder">
|
||||
<ion-icon :icon="alarm" style="font-size: 24px; color: var(--text-primary)"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-text style="font-size: 12px; color: var(--text-primary)">提醒</ion-text>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; align-items: center">
|
||||
<ion-button fill="clear" @click="handleDelete">
|
||||
<ion-icon :icon="trash" style="font-size: 24px; color: var(--text-primary)"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-text style="font-size: 12px; color: var(--text-primary)">删除</ion-text>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue';
|
||||
import { useAppData } from '../utils/AppDataContext';
|
||||
import { star, starOutline, share, folder, alarm, trash } from 'ionicons/icons';
|
||||
import Header from '../components/Header.vue';
|
||||
|
||||
export default {
|
||||
name: 'NoteDetailPage',
|
||||
components: {
|
||||
Header
|
||||
},
|
||||
props: {
|
||||
noteId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const { state, updateNote } = useAppData();
|
||||
|
||||
// Find the note by ID
|
||||
const note = computed(() => state.notes.find(n => n.id === props.noteId));
|
||||
|
||||
const handleEdit = () => {
|
||||
// 导航到编辑页面的逻辑将在路由中处理
|
||||
window.location.hash = `#/editor/${props.noteId}`;
|
||||
};
|
||||
|
||||
const handleShare = () => {
|
||||
// In a full implementation, this would share the note
|
||||
console.log('Share note');
|
||||
};
|
||||
|
||||
const handleMoveToFolder = () => {
|
||||
// In a full implementation, this would move the note to a folder
|
||||
console.log('Move to folder');
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
// In a full implementation, this would delete the note
|
||||
console.log('Delete note');
|
||||
};
|
||||
|
||||
const handleStar = async () => {
|
||||
// Toggle star status
|
||||
if (note.value) {
|
||||
await updateNote(props.noteId, { isStarred: !note.value.isStarred });
|
||||
}
|
||||
};
|
||||
|
||||
const handleReminder = () => {
|
||||
// In a full implementation, this would set a reminder
|
||||
console.log('Set reminder');
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString();
|
||||
};
|
||||
|
||||
return {
|
||||
note: note.value,
|
||||
handleEdit,
|
||||
handleShare,
|
||||
handleMoveToFolder,
|
||||
handleDelete,
|
||||
handleStar,
|
||||
handleReminder,
|
||||
formatDate,
|
||||
star,
|
||||
starOutline,
|
||||
share,
|
||||
folder,
|
||||
alarm,
|
||||
trash
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
197
src/pages/NoteEditorPage.vue
Normal file
197
src/pages/NoteEditorPage.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<ion-page>
|
||||
<Header
|
||||
:title="isEditing ? '编辑便签' : '新建便签'"
|
||||
:onBack="handleCancel"
|
||||
:onAction="handleSave"
|
||||
actionText="保存"
|
||||
/>
|
||||
<div style="display: flex; border-bottom: 1px solid var(--border); padding: 8px 16px; background-color: var(--background-card)">
|
||||
<ion-button fill="clear" @click="handleBold" style="padding: 8px; margin-right: 8px">
|
||||
<img src="/assets/icons/drawable-xxhdpi/rtf_bold_normal.9.png" style="width: 24px; height: 24px" />
|
||||
</ion-button>
|
||||
<ion-button fill="clear" @click="handleItalic" style="padding: 8px; margin-right: 8px">
|
||||
<img src="/assets/icons/drawable-xxhdpi/rtf_gtasks_normal.9.png" style="width: 24px; height: 24px" />
|
||||
</ion-button>
|
||||
<ion-button fill="clear" @click="handleUnderline" style="padding: 8px; margin-right: 8px">
|
||||
<img src="/assets/icons/drawable-xxhdpi/rtf_list_normal.9.png" style="width: 24px; height: 24px" />
|
||||
</ion-button>
|
||||
<ion-button fill="clear" @click="handleList" style="padding: 8px; margin-right: 8px">
|
||||
<img src="/assets/icons/drawable-xxhdpi/rtf_header_normal.9.png" style="width: 24px; height: 24px" />
|
||||
</ion-button>
|
||||
<ion-button fill="clear" @click="handleHeader" style="padding: 8px; margin-right: 8px">
|
||||
<img src="/assets/icons/drawable-xxhdpi/rtf_quot_normal.9.png" style="width: 24px; height: 24px" />
|
||||
</ion-button>
|
||||
<ion-button fill="clear" @click="handleQuote" style="padding: 8px">
|
||||
<img src="/assets/icons/drawable-xxhdpi/rtf_center_normal.9.png" style="width: 24px; height: 24px" />
|
||||
</ion-button>
|
||||
</div>
|
||||
<ion-content>
|
||||
<div style="padding: 16px; background-color: var(--background-card)">
|
||||
<textarea
|
||||
placeholder="标题"
|
||||
:value="title"
|
||||
@input="e => setTitle(e.target.value)"
|
||||
style="font-size: 22px; font-weight: 600; color: var(--note-title); margin-bottom: 16px; padding: 8px 0; border-bottom: 1px solid var(--border); width: 100%; background-color: transparent; border: none; outline: none; resize: none; font-family: inherit"
|
||||
></textarea>
|
||||
<textarea
|
||||
placeholder="开始写作..."
|
||||
:value="content"
|
||||
@input="e => setContent(e.target.value)"
|
||||
style="font-size: 16px; color: var(--note-content); line-height: 24px; width: 100%; height: calc(100vh - 200px); background-color: transparent; border: none; outline: none; resize: none; font-family: inherit"
|
||||
></textarea>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
||||
<ion-alert
|
||||
:is-open="showAlert"
|
||||
@didDismiss="() => setShowAlert(false)"
|
||||
header="未保存的更改"
|
||||
message="您有未保存的更改,确定要丢弃吗?"
|
||||
:buttons="[
|
||||
{
|
||||
text: '取消',
|
||||
role: 'cancel'
|
||||
},
|
||||
{
|
||||
text: '丢弃',
|
||||
handler: () => window.history.back()
|
||||
}
|
||||
]"
|
||||
></ion-alert>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAppData } from '../utils/AppDataContext';
|
||||
import { save, text, list } from 'ionicons/icons';
|
||||
import Header from '../components/Header.vue';
|
||||
|
||||
export default {
|
||||
name: 'NoteEditorPage',
|
||||
components: {
|
||||
Header
|
||||
},
|
||||
props: {
|
||||
noteId: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const { state, addNote, updateNote } = useAppData();
|
||||
|
||||
// Check if we're editing an existing note
|
||||
const isEditing = !!props.noteId;
|
||||
const existingNote = isEditing ? state.notes.find(n => n.id === props.noteId) : null;
|
||||
|
||||
// Initialize state with existing note data or empty strings
|
||||
const title = ref(existingNote?.title || '');
|
||||
const content = ref(existingNote?.content || '');
|
||||
const showAlert = ref(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
// Validate input
|
||||
if (!title.value.trim()) {
|
||||
// In a full implementation, show an alert or toast
|
||||
console.log('Validation error: Please enter a note title.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isEditing && existingNote) {
|
||||
// Update existing note
|
||||
await updateNote(props.noteId, { title: title.value, content: content.value });
|
||||
} else {
|
||||
// Create new note
|
||||
await addNote({ title: title.value, content: content.value, isStarred: false });
|
||||
}
|
||||
|
||||
// Navigate back to the previous screen
|
||||
window.history.back();
|
||||
} catch (error) {
|
||||
// In a full implementation, show an alert or toast
|
||||
console.log('Save error: Failed to save note. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
// Check if there are unsaved changes
|
||||
const hasUnsavedChanges =
|
||||
title.value !== (existingNote?.title || '') ||
|
||||
content.value !== (existingNote?.content || '');
|
||||
|
||||
if (hasUnsavedChanges) {
|
||||
showAlert.value = true;
|
||||
} else {
|
||||
window.history.back();
|
||||
}
|
||||
};
|
||||
|
||||
// Toolbar actions
|
||||
const handleBold = () => {
|
||||
// In a full implementation, this would apply bold formatting
|
||||
console.log('Bold pressed');
|
||||
};
|
||||
|
||||
const handleItalic = () => {
|
||||
// In a full implementation, this would apply italic formatting
|
||||
console.log('Italic pressed');
|
||||
};
|
||||
|
||||
const handleUnderline = () => {
|
||||
// In a full implementation, this would apply underline formatting
|
||||
console.log('Underline pressed');
|
||||
};
|
||||
|
||||
const handleList = () => {
|
||||
// In a full implementation, this would insert a list
|
||||
console.log('List pressed');
|
||||
};
|
||||
|
||||
const handleHeader = () => {
|
||||
// In a full implementation, this would apply header formatting
|
||||
console.log('Header pressed');
|
||||
};
|
||||
|
||||
const handleQuote = () => {
|
||||
// In a full implementation, this would apply quote formatting
|
||||
console.log('Quote pressed');
|
||||
};
|
||||
|
||||
const setTitle = (value) => {
|
||||
title.value = value;
|
||||
};
|
||||
|
||||
const setContent = (value) => {
|
||||
content.value = value;
|
||||
};
|
||||
|
||||
const setShowAlert = (value) => {
|
||||
showAlert.value = value;
|
||||
};
|
||||
|
||||
return {
|
||||
isEditing,
|
||||
title,
|
||||
content,
|
||||
showAlert,
|
||||
handleSave,
|
||||
handleCancel,
|
||||
handleBold,
|
||||
handleItalic,
|
||||
handleUnderline,
|
||||
handleList,
|
||||
handleHeader,
|
||||
handleQuote,
|
||||
setTitle,
|
||||
setContent,
|
||||
setShowAlert,
|
||||
save,
|
||||
text,
|
||||
list
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
307
src/pages/NoteListPage.vue
Normal file
307
src/pages/NoteListPage.vue
Normal file
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<ion-page>
|
||||
<Header
|
||||
:title="headerTitle"
|
||||
:onAction="handleAddNote"
|
||||
actionIcon="create"
|
||||
leftIcon="settings"
|
||||
:onLeftAction="handleSettingsPress"
|
||||
:onFolderToggle="handleFolderToggle"
|
||||
:isFolderExpanded="isFolderExpanded"
|
||||
:onTitlePress="handleFolderToggle"
|
||||
/>
|
||||
|
||||
<!-- 悬浮文件夹列表 - 使用绝对定位实现 -->
|
||||
<div
|
||||
v-if="isFolderExpanded"
|
||||
style="position: absolute; top: 50px; left: 10%; right: 10%; z-index: 1000; background-color: var(--background-card); border-radius: 8px; box-shadow: 0 2px 4px var(--shadow); border: 1px solid var(--border); overflow: hidden"
|
||||
>
|
||||
<FolderItem
|
||||
id="all"
|
||||
name="全部便签"
|
||||
:noteCount="notes.length"
|
||||
:isSelected="currentFolder === 'all'"
|
||||
:onPress="() => {
|
||||
setCurrentFolder('all');
|
||||
setIsFolderExpanded(false);
|
||||
}"
|
||||
/>
|
||||
<FolderItem
|
||||
id="starred"
|
||||
name="加星便签"
|
||||
:noteCount="starredNotesCount"
|
||||
:isSelected="currentFolder === 'starred'"
|
||||
:onPress="() => {
|
||||
setCurrentFolder('starred');
|
||||
setIsFolderExpanded(false);
|
||||
}"
|
||||
/>
|
||||
<FolderItem
|
||||
id="trash"
|
||||
name="回收站"
|
||||
:noteCount="0"
|
||||
:isSelected="currentFolder === 'trash'"
|
||||
:onPress="() => {
|
||||
setCurrentFolder('trash');
|
||||
setIsFolderExpanded(false);
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="flex-direction: row; align-items: center; padding: 8px 16px; background-color: var(--background-card); border-bottom: 1px solid var(--border)">
|
||||
<div style="flex: 1; flex-direction: row; align-items: center; background-color: var(--background-card); height: 36px; padding: 0 8px; padding-vertical: 0">
|
||||
<div style="flex: 1; flex-direction: row; align-items: center; background-color: #f0f0f0; border-radius: 4; height: 36px; padding: 0 8px; padding-vertical: 0">
|
||||
<img :src="'/assets/icons/drawable-xxhdpi/search_bar_left_icon.png'" style="width: 20px; height: 20px; tint-color: var(--text-tertiary)" />
|
||||
<input
|
||||
placeholder="搜索便签..."
|
||||
:value="searchQuery"
|
||||
@input="e => setSearchQuery(e.target.value)"
|
||||
@keydown.enter="handleSearch"
|
||||
style="flex: 1; font-size: 16px; color: var(--text-primary); margin-left: 8px; margin-right: 8px; padding: 0; border: none; background: transparent; outline: none"
|
||||
/>
|
||||
<ion-button
|
||||
v-if="searchQuery.length > 0"
|
||||
fill="clear"
|
||||
@click="() => setSearchQuery('')"
|
||||
style="--padding-start: 0; --padding-end: 0; width: 20px; height: 20px; margin: 0; padding: 0"
|
||||
>
|
||||
<img :src="'/assets/icons/drawable-xxhdpi/search_bar_clear_btn.png'" style="width: 20px; height: 20px; tint-color: var(--text-tertiary)" />
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredAndSortedNotes.length === 0" style="flex: 1; justify-content: center; align-items: center; padding: 16px; background-color: var(--background)">
|
||||
<ion-text style="font-size: 18px; font-weight: 600; color: var(--text-tertiary); margin-bottom: 8px">
|
||||
未找到便签
|
||||
</ion-text>
|
||||
<ion-text style="font-size: 14px; color: var(--text-tertiary); text-align: center; line-height: 20px">
|
||||
{{ searchQuery ? '尝试其他搜索词' : '点击 + 按钮创建您的第一条便签' }}
|
||||
</ion-text>
|
||||
</div>
|
||||
<div v-else style="flex: 1; background-color: var(--background)">
|
||||
<ion-text style="font-size: 13px; color: var(--text-tertiary); padding-horizontal: 16px; padding-vertical: 8px; display: block">
|
||||
{{ filteredAndSortedNotes.length }} 条便签
|
||||
</ion-text>
|
||||
<div style="border-radius: 6px; overflow: hidden; box-shadow: 0 1px 2px var(--shadow); background-color: var(--background-card); margin: 0 16px">
|
||||
<div v-for="note in filteredAndSortedNotes" :key="note.id">
|
||||
<NoteItem
|
||||
:title="note.title"
|
||||
:content="note.content"
|
||||
:date="formatDate(note.updatedAt)"
|
||||
:isStarred="note.isStarred"
|
||||
:onPress="() => handleNotePress(note.id)"
|
||||
:onDelete="() => handleDeleteNote(note.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 点击外部区域收起文件夹列表的覆盖层 -->
|
||||
<div
|
||||
v-if="isFolderExpanded"
|
||||
@click="() => setIsFolderExpanded(false)"
|
||||
style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: transparent; z-index: 99"
|
||||
></div>
|
||||
|
||||
<ion-alert
|
||||
:is-open="showAlert"
|
||||
@didDismiss="() => setShowAlert(false)"
|
||||
header="删除便签"
|
||||
message="确定要删除这个便签吗?"
|
||||
:buttons="[
|
||||
{
|
||||
text: '取消',
|
||||
role: 'cancel'
|
||||
},
|
||||
{
|
||||
text: '删除',
|
||||
handler: confirmDeleteNote
|
||||
}
|
||||
]"
|
||||
></ion-alert>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAppData } from '../utils/AppDataContext';
|
||||
import { create, settings } from 'ionicons/icons';
|
||||
import NoteItem from '../components/NoteItem.vue';
|
||||
import Header from '../components/Header.vue';
|
||||
import FolderItem from '../components/FolderItem.vue';
|
||||
|
||||
export default {
|
||||
name: 'NoteListPage',
|
||||
components: {
|
||||
NoteItem,
|
||||
Header,
|
||||
FolderItem
|
||||
},
|
||||
setup() {
|
||||
const { state, deleteNote } = useAppData();
|
||||
const searchQuery = ref('');
|
||||
const sortBy = ref('date'); // 'date', 'title', 'starred'
|
||||
const isFolderExpanded = ref(false);
|
||||
const currentFolder = ref('all'); // 默认文件夹是"全部便签"
|
||||
const showAlert = ref(false);
|
||||
const noteToDelete = ref(null);
|
||||
|
||||
// 计算加星便签数量
|
||||
const starredNotesCount = computed(() => {
|
||||
return state.notes.filter(note => note.isStarred).length;
|
||||
});
|
||||
|
||||
// 根据当前文件夹过滤便签
|
||||
const filteredNotes = computed(() => {
|
||||
return state.notes.filter(note => {
|
||||
switch (currentFolder.value) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'starred':
|
||||
return note.isStarred;
|
||||
case 'trash':
|
||||
// 假设我们有一个isDeleted属性来标识已删除的便签
|
||||
return note.isDeleted || false;
|
||||
default:
|
||||
return note.folderId === currentFolder.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Filter and sort notes
|
||||
const filteredAndSortedNotes = computed(() => {
|
||||
return filteredNotes.value
|
||||
.filter(note =>
|
||||
note.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
note.content.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (sortBy.value === 'title') {
|
||||
return a.title.localeCompare(b.title);
|
||||
} else if (sortBy.value === 'starred') {
|
||||
return (b.isStarred ? 1 : 0) - (a.isStarred ? 1 : 0);
|
||||
} else {
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 计算头部标题
|
||||
const headerTitle = computed(() => {
|
||||
switch (currentFolder.value) {
|
||||
case 'all':
|
||||
return '全部便签';
|
||||
case 'starred':
|
||||
return '加星便签';
|
||||
case 'trash':
|
||||
return '回收站';
|
||||
default:
|
||||
return '文件夹';
|
||||
}
|
||||
});
|
||||
|
||||
const handleNotePress = (noteId) => {
|
||||
// 导航到详情页面的逻辑将在路由中处理
|
||||
window.location.hash = `#/notes/${noteId}`;
|
||||
};
|
||||
|
||||
const handleAddNote = () => {
|
||||
// 导航到编辑页面的逻辑将在路由中处理
|
||||
window.location.hash = '#/editor';
|
||||
};
|
||||
|
||||
const handleDeleteNote = (noteId) => {
|
||||
noteToDelete.value = noteId;
|
||||
showAlert.value = true;
|
||||
};
|
||||
|
||||
const confirmDeleteNote = () => {
|
||||
if (noteToDelete.value) {
|
||||
deleteNote(noteToDelete.value);
|
||||
noteToDelete.value = null;
|
||||
}
|
||||
showAlert.value = false;
|
||||
};
|
||||
|
||||
const handleSort = () => {
|
||||
// In a full implementation, this would cycle through sort options
|
||||
const sortOptions = ['date', 'title', 'starred'];
|
||||
const currentIndex = sortOptions.indexOf(sortBy.value);
|
||||
const nextIndex = (currentIndex + 1) % sortOptions.length;
|
||||
sortBy.value = sortOptions[nextIndex];
|
||||
console.log('Sort by:', sortOptions[nextIndex]);
|
||||
};
|
||||
|
||||
const handleFolderPress = () => {
|
||||
// 导航到文件夹页面的逻辑将在路由中处理
|
||||
window.location.hash = '#/folders';
|
||||
};
|
||||
|
||||
const handleSettingsPress = () => {
|
||||
// 导航到设置页面的逻辑将在路由中处理
|
||||
window.location.hash = '#/settings';
|
||||
};
|
||||
|
||||
const handleFolderToggle = () => {
|
||||
// 在实际应用中,这里会触发文件夹列表的展开/收起
|
||||
isFolderExpanded.value = !isFolderExpanded.value;
|
||||
console.log('Folder expanded:', !isFolderExpanded.value);
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
// In a full implementation, this would filter notes based on searchQuery
|
||||
console.log('Search for:', searchQuery.value);
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString();
|
||||
};
|
||||
|
||||
const setCurrentFolder = (folder) => {
|
||||
currentFolder.value = folder;
|
||||
};
|
||||
|
||||
const setIsFolderExpanded = (expanded) => {
|
||||
isFolderExpanded.value = expanded;
|
||||
};
|
||||
|
||||
const setSearchQuery = (query) => {
|
||||
searchQuery.value = query;
|
||||
};
|
||||
|
||||
const setShowAlert = (show) => {
|
||||
showAlert.value = show;
|
||||
};
|
||||
|
||||
return {
|
||||
notes: state.notes,
|
||||
searchQuery,
|
||||
sortBy,
|
||||
isFolderExpanded,
|
||||
currentFolder,
|
||||
showAlert,
|
||||
noteToDelete,
|
||||
starredNotesCount,
|
||||
filteredAndSortedNotes,
|
||||
headerTitle,
|
||||
handleNotePress,
|
||||
handleAddNote,
|
||||
handleDeleteNote,
|
||||
confirmDeleteNote,
|
||||
handleSearch,
|
||||
handleSort,
|
||||
handleFolderPress,
|
||||
handleSettingsPress,
|
||||
handleFolderToggle,
|
||||
formatDate,
|
||||
setCurrentFolder,
|
||||
setIsFolderExpanded,
|
||||
setSearchQuery,
|
||||
setShowAlert,
|
||||
create,
|
||||
settings
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
156
src/pages/SettingsPage.vue
Normal file
156
src/pages/SettingsPage.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<ion-page>
|
||||
<Header
|
||||
title="设置"
|
||||
:onBack="handleBackPress"
|
||||
/>
|
||||
|
||||
<ion-content style="background-color: var(--background)">
|
||||
<div style="margin-bottom: 12px; background-color: var(--background-card)">
|
||||
<div style="background-color: var(--background-secondary); font-size: 13px; font-weight: 600; color: var(--text-tertiary); padding: 10px 16px">
|
||||
账户
|
||||
</div>
|
||||
<div button @click="handleLogin" style="display: flex; justify-content: space-between; align-items: center; background-color: var(--background-card); padding: 14px 16px; border-bottom: 1px solid var(--border); cursor: pointer">
|
||||
<div style="font-size: 16px; color: var(--text-primary)">登录云同步</div>
|
||||
<div style="font-size: 15px; color: var(--text-tertiary)">未登录</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px; background-color: var(--background-card)">
|
||||
<div style="background-color: var(--background-secondary); font-size: 13px; font-weight: 600; color: var(--text-tertiary); padding: 10px 16px">
|
||||
偏好设置
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; background-color: var(--background-card); padding: 14px 16px; border-bottom: 1px solid var(--border)">
|
||||
<div style="font-size: 16px; color: var(--text-primary)">云同步</div>
|
||||
<ion-toggle
|
||||
slot="end"
|
||||
:checked="settings.cloudSync"
|
||||
@ion-change="toggleCloudSync"
|
||||
></ion-toggle>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; background-color: var(--background-card); padding: 14px 16px">
|
||||
<div style="font-size: 16px; color: var(--text-primary)">深色模式</div>
|
||||
<ion-toggle
|
||||
slot="end"
|
||||
:checked="settings.darkMode"
|
||||
@ion-change="toggleDarkMode"
|
||||
></ion-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px; background-color: var(--background-card)">
|
||||
<div style="background-color: var(--background-secondary); font-size: 13px; font-weight: 600; color: var(--text-tertiary); padding: 10px 16px">
|
||||
数据管理
|
||||
</div>
|
||||
<div button @click="handleBackup" style="display: flex; align-items: center; background-color: var(--background-card); padding: 14px 16px; border-bottom: 1px solid var(--border); cursor: pointer">
|
||||
<img :src="'/assets/icons/drawable-xxhdpi/btn_save_pic.png'" style="width: 20px; height: 20px; color: var(--text-primary); margin-right: 12px" />
|
||||
<div style="font-size: 16px; color: var(--text-primary)">备份便签</div>
|
||||
</div>
|
||||
<div button @click="handleRestore" style="display: flex; align-items: center; background-color: var(--background-card); padding: 14px 16px; border-bottom: 1px solid var(--border); cursor: pointer">
|
||||
<img :src="'/assets/icons/drawable-xxhdpi/btn_restore.png'" style="width: 20px; height: 20px; color: var(--text-primary); margin-right: 12px" />
|
||||
<div style="font-size: 16px; color: var(--text-primary)">恢复便签</div>
|
||||
</div>
|
||||
<div button @click="handleExport" style="display: flex; align-items: center; background-color: var(--background-card); padding: 14px 16px; border-bottom: 1px solid var(--border); cursor: pointer">
|
||||
<img :src="'/assets/icons/drawable-xxhdpi/btn_share.png'" style="width: 20px; height: 20px; color: var(--text-primary); margin-right: 12px" />
|
||||
<div style="font-size: 16px; color: var(--text-primary)">导出便签</div>
|
||||
</div>
|
||||
<div button @click="handleImport" style="display: flex; align-items: center; background-color: var(--background-card); padding: 14px 16px; cursor: pointer">
|
||||
<img :src="'/assets/icons/drawable-xxhdpi/btn_load_error.png'" style="width: 20px; height: 20px; color: var(--text-primary); margin-right: 12px" />
|
||||
<div style="font-size: 16px; color: var(--text-primary)">导入便签</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px; background-color: var(--background-card)">
|
||||
<div style="background-color: var(--background-secondary); font-size: 13px; font-weight: 600; color: var(--text-tertiary); padding: 10px 16px">
|
||||
关于
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; background-color: var(--background-card); padding: 14px 16px; border-bottom: 1px solid var(--border)">
|
||||
<div style="font-size: 16px; color: var(--text-primary)">版本</div>
|
||||
<div style="font-size: 15px; color: var(--text-tertiary)">1.0.0</div>
|
||||
</div>
|
||||
<div button @click="handlePrivacyPolicy" style="display: flex; justify-content: space-between; align-items: center; background-color: var(--background-card); padding: 14px 16px; border-bottom: 1px solid var(--border); cursor: pointer">
|
||||
<div style="font-size: 16px; color: var(--text-primary)">隐私政策</div>
|
||||
</div>
|
||||
<div button @click="handleTermsOfService" style="display: flex; justify-content: space-between; align-items: center; background-color: var(--background-card); padding: 14px 16px; cursor: pointer">
|
||||
<div style="font-size: 16px; color: var(--text-primary)">服务条款</div>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAppData } from '../utils/AppDataContext';
|
||||
import Header from '../components/Header.vue';
|
||||
|
||||
export default {
|
||||
name: 'SettingsPage',
|
||||
components: {
|
||||
Header
|
||||
},
|
||||
setup() {
|
||||
const { state, updateSettings } = useAppData();
|
||||
|
||||
const toggleCloudSync = () => {
|
||||
updateSettings({ cloudSync: !state.settings.cloudSync });
|
||||
};
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
updateSettings({ darkMode: !state.settings.darkMode });
|
||||
};
|
||||
|
||||
const handleLogin = () => {
|
||||
// In a full implementation, this would open a login screen
|
||||
console.log('Login to cloud');
|
||||
};
|
||||
|
||||
const handlePrivacyPolicy = () => {
|
||||
// In a full implementation, this would show the privacy policy
|
||||
console.log('Privacy policy');
|
||||
};
|
||||
|
||||
const handleTermsOfService = () => {
|
||||
// In a full implementation, this would show the terms of service
|
||||
console.log('Terms of service');
|
||||
};
|
||||
|
||||
const handleBackup = () => {
|
||||
// In a full implementation, this would backup notes
|
||||
console.log('Backup notes');
|
||||
};
|
||||
|
||||
const handleRestore = () => {
|
||||
// In a full implementation, this would restore notes
|
||||
console.log('Restore notes');
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
// In a full implementation, this would export notes
|
||||
console.log('Export notes');
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
// In a full implementation, this would import notes
|
||||
console.log('Import notes');
|
||||
};
|
||||
|
||||
const handleBackPress = () => {
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
return {
|
||||
settings: state.settings,
|
||||
toggleCloudSync,
|
||||
toggleDarkMode,
|
||||
handleLogin,
|
||||
handlePrivacyPolicy,
|
||||
handleTermsOfService,
|
||||
handleBackup,
|
||||
handleRestore,
|
||||
handleExport,
|
||||
handleImport,
|
||||
handleBackPress
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
119
src/utils/AppDataContext.js
Normal file
119
src/utils/AppDataContext.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import { reactive, provide, inject, watch } from 'vue';
|
||||
import * as storage from './storage';
|
||||
|
||||
// Define the context key
|
||||
const AppDataContextKey = Symbol('AppDataContext');
|
||||
|
||||
// Create the provider component
|
||||
export const AppDataProvider = {
|
||||
setup(props, { slots }) {
|
||||
// Initialize reactive state
|
||||
const state = reactive({
|
||||
notes: [],
|
||||
folders: [],
|
||||
settings: { cloudSync: false, darkMode: false }
|
||||
});
|
||||
|
||||
// Load data on app start
|
||||
const loadData = async () => {
|
||||
const loadedNotes = await storage.getNotes();
|
||||
const loadedFolders = await storage.getFolders();
|
||||
const loadedSettings = await storage.getSettings();
|
||||
|
||||
state.notes = loadedNotes;
|
||||
state.folders = loadedFolders;
|
||||
state.settings = loadedSettings;
|
||||
};
|
||||
|
||||
// Save notes when they change
|
||||
watch(
|
||||
() => state.notes,
|
||||
(newNotes) => {
|
||||
storage.saveNotes(newNotes);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Save folders when they change
|
||||
watch(
|
||||
() => state.folders,
|
||||
(newFolders) => {
|
||||
storage.saveFolders(newFolders);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Save settings when they change
|
||||
watch(
|
||||
() => state.settings,
|
||||
(newSettings) => {
|
||||
storage.saveSettings(newSettings);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Note functions
|
||||
const addNote = async (note) => {
|
||||
const newNote = await storage.addNote(note);
|
||||
state.notes.push(newNote);
|
||||
return newNote;
|
||||
};
|
||||
|
||||
const updateNote = async (id, updates) => {
|
||||
const updatedNote = await storage.updateNote(id, updates);
|
||||
if (updatedNote) {
|
||||
const index = state.notes.findIndex(note => note.id === id);
|
||||
if (index !== -1) {
|
||||
state.notes[index] = updatedNote;
|
||||
}
|
||||
}
|
||||
return updatedNote;
|
||||
};
|
||||
|
||||
const deleteNote = async (id) => {
|
||||
const result = await storage.deleteNote(id);
|
||||
if (result) {
|
||||
state.notes = state.notes.filter(note => note.id !== id);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Folder functions
|
||||
const addFolder = async (folder) => {
|
||||
const newFolder = await storage.addFolder(folder);
|
||||
state.folders.push(newFolder);
|
||||
return newFolder;
|
||||
};
|
||||
|
||||
// Settings functions
|
||||
const updateSettings = async (newSettings) => {
|
||||
const updatedSettings = { ...state.settings, ...newSettings };
|
||||
state.settings = updatedSettings;
|
||||
await storage.saveSettings(updatedSettings);
|
||||
};
|
||||
|
||||
// Load initial data
|
||||
loadData();
|
||||
|
||||
// Provide the context
|
||||
provide(AppDataContextKey, {
|
||||
state,
|
||||
addNote,
|
||||
updateNote,
|
||||
deleteNote,
|
||||
addFolder,
|
||||
updateSettings
|
||||
});
|
||||
|
||||
return () => slots.default?.();
|
||||
}
|
||||
};
|
||||
|
||||
// Hook to use the context
|
||||
export const useAppData = () => {
|
||||
const context = inject(AppDataContextKey);
|
||||
if (!context) {
|
||||
throw new Error('useAppData must be used within an AppDataProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
74
src/utils/colors.js
Normal file
74
src/utils/colors.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// Smartisan Notes Color Scheme - Based on Original Design
|
||||
export default {
|
||||
// Primary colors - Original Smartisan Notes brown/gold palette
|
||||
primary: '#5c3c2a', // Main brown color for UI elements
|
||||
primaryDark: '#4a3224', // Darker shade of primary
|
||||
primaryLight: '#f5f0e6', // Light background tone
|
||||
|
||||
// Background colors - Warm paper-like tones
|
||||
background: '#fbf7ed', // Main app background - warm off-white
|
||||
backgroundSecondary: '#f7f2e9', // Slightly darker background
|
||||
backgroundCard: '#ffffff', // Pure white for cards/notes
|
||||
searchBarBackground: '#f0f0f0', // Search bar background - light gray
|
||||
|
||||
// Text colors - Brown/black tones for readability
|
||||
textPrimary: '#5c3c2a', // Main text color - dark brown
|
||||
textSecondary: '#6e482f', // Secondary text - medium brown
|
||||
textTertiary: '#9e836c', // Tertiary text - light brown/gray
|
||||
textInverted: '#ffffff', // White text for dark backgrounds
|
||||
|
||||
// Accent colors - Smartisan's signature colors
|
||||
accentBlue: '#5c89f2', // Blue for links/actions
|
||||
accentGreen: '#97cc4e', // Green for success/positive actions
|
||||
accentRed: '#e65c53', // Red for errors/dangerous actions
|
||||
accentOrange: '#f0880d', // Orange for warnings/highlights
|
||||
accentYellow: '#ffd633', // Yellow for starred items/highlights (updated to match original)
|
||||
|
||||
// Note specific colors
|
||||
noteTitle: '#5c3c2a', // Note title color
|
||||
noteContent: '#6e482f', // Note content color
|
||||
noteDate: '#b9a691', // Date/time color
|
||||
noteStar: '#ffd633', // Star/favorite color (updated to match original)
|
||||
|
||||
// Folder colors
|
||||
folderName: '#5c3c2a', // Folder name color
|
||||
folderCount: '#99000000', // Folder item count color (60% black)
|
||||
folderItemSelected: '#f0f0f0', // Folder item selected background color
|
||||
|
||||
// Button colors - Based on Smartisan's button styles
|
||||
buttonPrimary: '#5c3c2a', // Primary button - brown
|
||||
buttonSecondary: '#97cc4e', // Secondary button - green
|
||||
buttonDanger: '#e65c53', // Danger button - red
|
||||
buttonDisabled: '#d4d4d5', // Disabled button - light gray
|
||||
|
||||
// Status colors
|
||||
success: '#79ad31', // Success - green
|
||||
warning: '#f0880d', // Warning - orange
|
||||
error: '#e64746', // Error - red
|
||||
info: '#5c89f2', // Info - blue
|
||||
|
||||
// UI elements - Borders, dividers, shadows
|
||||
border: '#e5ddca', // Light brown border
|
||||
divider: '#e5e5e5', // Light gray divider
|
||||
shadow: '#00000014', // Subtle shadow
|
||||
|
||||
// Transparency variants
|
||||
black05: '#0000000d', // 5% black
|
||||
black10: '#0000001a', // 10% black
|
||||
black20: '#00000033', // 20% black
|
||||
black30: '#0000004d', // 30% black
|
||||
black40: '#00000066', // 40% black
|
||||
black50: '#00000080', // 50% black
|
||||
black60: '#00000099', // 60% black
|
||||
black80: '#000000cc', // 80% black
|
||||
black90: '#000000e6', // 90% black
|
||||
|
||||
white10: '#ffffff1a', // 10% white
|
||||
white20: '#ffffff33', // 20% white
|
||||
white30: '#ffffff4d', // 30% white
|
||||
white40: '#ffffff66', // 40% white
|
||||
white50: '#ffffff80', // 50% white
|
||||
white60: '#ffffff99', // 60% white
|
||||
white80: '#ffffffcc', // 80% white
|
||||
white90: '#ffffffe6', // 90% white
|
||||
};
|
||||
118
src/utils/storage.js
Normal file
118
src/utils/storage.js
Normal file
@@ -0,0 +1,118 @@
|
||||
// Storage keys
|
||||
const NOTES_KEY = 'notes';
|
||||
const FOLDERS_KEY = 'folders';
|
||||
const SETTINGS_KEY = 'settings';
|
||||
|
||||
// Notes functions
|
||||
export const getNotes = async () => {
|
||||
try {
|
||||
const notesJson = localStorage.getItem(NOTES_KEY);
|
||||
return notesJson ? JSON.parse(notesJson) : [];
|
||||
} catch (error) {
|
||||
console.error('Error getting notes:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const saveNotes = async (notes) => {
|
||||
try {
|
||||
localStorage.setItem(NOTES_KEY, JSON.stringify(notes));
|
||||
} catch (error) {
|
||||
console.error('Error saving notes:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const addNote = async (note) => {
|
||||
const newNote = {
|
||||
...note,
|
||||
id: Date.now().toString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const notes = await getNotes();
|
||||
notes.push(newNote);
|
||||
await saveNotes(notes);
|
||||
|
||||
return newNote;
|
||||
};
|
||||
|
||||
export const updateNote = async (id, updates) => {
|
||||
const notes = await getNotes();
|
||||
const index = notes.findIndex(note => note.id === id);
|
||||
|
||||
if (index === -1) return null;
|
||||
|
||||
const updatedNote = {
|
||||
...notes[index],
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
notes[index] = updatedNote;
|
||||
await saveNotes(notes);
|
||||
|
||||
return updatedNote;
|
||||
};
|
||||
|
||||
export const deleteNote = async (id) => {
|
||||
const notes = await getNotes();
|
||||
const filteredNotes = notes.filter(note => note.id !== id);
|
||||
|
||||
if (notes.length === filteredNotes.length) return false;
|
||||
|
||||
await saveNotes(filteredNotes);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Folders functions
|
||||
export const getFolders = async () => {
|
||||
try {
|
||||
const foldersJson = localStorage.getItem(FOLDERS_KEY);
|
||||
return foldersJson ? JSON.parse(foldersJson) : [];
|
||||
} catch (error) {
|
||||
console.error('Error getting folders:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const saveFolders = async (folders) => {
|
||||
try {
|
||||
localStorage.setItem(FOLDERS_KEY, JSON.stringify(folders));
|
||||
} catch (error) {
|
||||
console.error('Error saving folders:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const addFolder = async (folder) => {
|
||||
const newFolder = {
|
||||
...folder,
|
||||
id: Date.now().toString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const folders = await getFolders();
|
||||
folders.push(newFolder);
|
||||
await saveFolders(folders);
|
||||
|
||||
return newFolder;
|
||||
};
|
||||
|
||||
// Settings functions
|
||||
export const getSettings = async () => {
|
||||
try {
|
||||
const settingsJson = localStorage.getItem(SETTINGS_KEY);
|
||||
return settingsJson ? JSON.parse(settingsJson) : { cloudSync: false, darkMode: false };
|
||||
} catch (error) {
|
||||
console.error('Error getting settings:', error);
|
||||
return { cloudSync: false, darkMode: false };
|
||||
}
|
||||
};
|
||||
|
||||
export const saveSettings = async (settings) => {
|
||||
try {
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
}
|
||||
};
|
||||
574
src/utils/styles.js
Normal file
574
src/utils/styles.js
Normal file
@@ -0,0 +1,574 @@
|
||||
// Styles for Smartisan Notes - Based on React Native version
|
||||
export default {
|
||||
// Common styles - Based on Smartisan Notes design principles
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: 'var(--background)',
|
||||
},
|
||||
|
||||
// Header styles - Warm, minimal design
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: 'var(--background)',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'var(--border)',
|
||||
},
|
||||
|
||||
headerTitleContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
headerTitleTouchable: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: 'var(--text-primary)',
|
||||
textAlign: 'center',
|
||||
},
|
||||
|
||||
headerFolderArrow: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
tintColor: 'var(--text-primary)',
|
||||
marginLeft: 8,
|
||||
},
|
||||
|
||||
headerButton: {
|
||||
padding: 8,
|
||||
},
|
||||
|
||||
headerButtonText: {
|
||||
fontSize: 16,
|
||||
color: 'var(--primary)',
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
||||
headerActionIcon: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
tintColor: 'var(--primary)',
|
||||
},
|
||||
|
||||
// Folder list styles
|
||||
folderListContainer: {
|
||||
position: 'absolute',
|
||||
top: 50,
|
||||
left: '10%',
|
||||
right: '10%',
|
||||
backgroundColor: 'var(--background-card)',
|
||||
borderRadius: 8,
|
||||
shadowColor: 'var(--shadow)',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
zIndex: 100,
|
||||
},
|
||||
|
||||
folderListItem: {
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'var(--border)',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
folderListItemActive: {
|
||||
backgroundColor: 'var(--folder-item-selected)',
|
||||
},
|
||||
|
||||
folderListItemText: {
|
||||
fontSize: 16,
|
||||
color: 'var(--text-primary)',
|
||||
},
|
||||
|
||||
folderListItemTextActive: {
|
||||
color: 'var(--primary)',
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
||||
folderListItemCount: {
|
||||
fontSize: 13,
|
||||
color: 'var(--text-tertiary)',
|
||||
},
|
||||
|
||||
// Note list styles - Clean, paper-like appearance
|
||||
noteListContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: 'var(--background)',
|
||||
},
|
||||
|
||||
searchContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
backgroundColor: 'var(--background-card)',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'var(--border)',
|
||||
},
|
||||
|
||||
searchInputContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'var(--background-card)',
|
||||
height: 36,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 0,
|
||||
},
|
||||
|
||||
searchInputBackground: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderRadius: 4,
|
||||
height: 36,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 0,
|
||||
},
|
||||
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
color: 'var(--text-primary)',
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
padding: 0,
|
||||
includeFontPadding: false,
|
||||
},
|
||||
|
||||
searchLeftIcon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
tintColor: 'var(--text-tertiary)',
|
||||
},
|
||||
|
||||
searchClearIcon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
tintColor: 'var(--text-tertiary)',
|
||||
},
|
||||
|
||||
noteListEmptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
backgroundColor: 'var(--background)',
|
||||
},
|
||||
|
||||
noteListEmptyText: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: 'var(--text-tertiary)',
|
||||
marginBottom: 8,
|
||||
},
|
||||
|
||||
noteListEmptySubtext: {
|
||||
fontSize: 14,
|
||||
color: 'var(--text-tertiary)',
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
},
|
||||
|
||||
noteCount: {
|
||||
fontSize: 13,
|
||||
color: 'var(--text-tertiary)',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
|
||||
// Note item styles - Paper note appearance with subtle shadows
|
||||
noteItem: {
|
||||
padding: 0,
|
||||
borderRadius: 6,
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: 'transparent',
|
||||
shadowColor: 'var(--shadow)',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'var(--background-card)',
|
||||
},
|
||||
|
||||
noteItemDeleteButton: {
|
||||
backgroundColor: 'var(--accent-red)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: 80,
|
||||
height: '100%',
|
||||
borderRadius: 6,
|
||||
marginBottom: 10,
|
||||
},
|
||||
|
||||
noteItemDeleteButtonImage: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
tintColor: 'var(--text-inverted)',
|
||||
},
|
||||
|
||||
noteItemHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 8,
|
||||
},
|
||||
|
||||
noteItemTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: 'var(--note-title)',
|
||||
flex: 1,
|
||||
marginRight: 8,
|
||||
},
|
||||
|
||||
noteItemStar: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
tintColor: 'var(--note-star)',
|
||||
},
|
||||
|
||||
noteItemContent: {
|
||||
fontSize: 14,
|
||||
color: 'var(--note-content)',
|
||||
marginBottom: 8,
|
||||
lineHeight: 20,
|
||||
includeFontPadding: false,
|
||||
},
|
||||
|
||||
noteItemDate: {
|
||||
fontSize: 12,
|
||||
color: 'var(--note-date)',
|
||||
includeFontPadding: false,
|
||||
},
|
||||
|
||||
// Floating action button - Circular button with warm color
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
backgroundColor: 'var(--primary)',
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 25,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
shadowColor: 'var(--shadow)',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 3,
|
||||
elevation: 3,
|
||||
},
|
||||
|
||||
fabIcon: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
tintColor: 'var(--text-inverted)',
|
||||
},
|
||||
|
||||
// Folder item styles - Clean list items with folder icon
|
||||
folderItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 1,
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: 'var(--accent-orange)',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'var(--background-card)',
|
||||
},
|
||||
|
||||
folderItemIcon: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
tintColor: 'var(--folder-name)',
|
||||
},
|
||||
|
||||
folderItemInfo: {
|
||||
flex: 1,
|
||||
marginLeft: 12,
|
||||
},
|
||||
|
||||
folderItemName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: 'var(--folder-name)',
|
||||
marginBottom: 2,
|
||||
},
|
||||
|
||||
folderItemCount: {
|
||||
fontSize: 13,
|
||||
color: 'var(--folder-count)',
|
||||
},
|
||||
|
||||
folderItemArrow: {
|
||||
fontSize: 18,
|
||||
color: 'var(--text-tertiary)',
|
||||
},
|
||||
|
||||
// Note editor styles - Clean writing surface
|
||||
noteEditorContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: 'var(--background-card)',
|
||||
},
|
||||
|
||||
editorToolbar: {
|
||||
flexDirection: 'row',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'var(--border)',
|
||||
backgroundColor: 'var(--background-card)',
|
||||
},
|
||||
|
||||
editorToolbarButton: {
|
||||
padding: 8,
|
||||
marginRight: 8,
|
||||
},
|
||||
|
||||
editorToolbarIcon: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
tintColor: 'var(--text-primary)',
|
||||
},
|
||||
|
||||
noteEditorContent: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
|
||||
noteEditorTitle: {
|
||||
fontSize: 22,
|
||||
fontWeight: '600',
|
||||
color: 'var(--note-title)',
|
||||
marginBottom: 16,
|
||||
paddingVertical: 8,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'var(--border)',
|
||||
},
|
||||
|
||||
noteEditorContentInput: {
|
||||
fontSize: 16,
|
||||
color: 'var(--note-content)',
|
||||
lineHeight: 24,
|
||||
flex: 1,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
|
||||
// Note detail styles - Clean reading experience
|
||||
noteDetailContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: 'var(--background-card)',
|
||||
},
|
||||
|
||||
noteDetailContent: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
|
||||
noteDetailHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
paddingVertical: 4,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'var(--border)',
|
||||
},
|
||||
|
||||
noteDetailDate: {
|
||||
fontSize: 13,
|
||||
color: 'var(--note-date)',
|
||||
},
|
||||
|
||||
noteDetailStarIcon: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
tintColor: 'var(--note-star)',
|
||||
},
|
||||
|
||||
noteDetailContentText: {
|
||||
fontSize: 16,
|
||||
color: 'var(--note-content)',
|
||||
lineHeight: 24,
|
||||
includeFontPadding: false,
|
||||
},
|
||||
|
||||
noteDetailFooter: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
paddingVertical: 12,
|
||||
backgroundColor: 'var(--background)',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: 'var(--border)',
|
||||
},
|
||||
|
||||
noteDetailActionButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 10,
|
||||
backgroundColor: 'var(--primary)',
|
||||
borderRadius: 4,
|
||||
},
|
||||
|
||||
noteDetailActionButtonText: {
|
||||
color: 'var(--text-inverted)',
|
||||
fontWeight: '500',
|
||||
fontSize: 15,
|
||||
marginLeft: 8,
|
||||
},
|
||||
|
||||
noteDetailActionIcon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
tintColor: 'var(--text-inverted)',
|
||||
},
|
||||
|
||||
// Settings styles - Clean, organized sections
|
||||
settingsSection: {
|
||||
backgroundColor: 'var(--background-card)',
|
||||
marginBottom: 12,
|
||||
},
|
||||
|
||||
settingsSectionTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: 'var(--text-tertiary)',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
backgroundColor: 'var(--background-secondary)',
|
||||
},
|
||||
|
||||
settingsItem: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'var(--border)',
|
||||
},
|
||||
|
||||
settingsItemWithIcon: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
settingsItemIcon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
tintColor: 'var(--text-primary)',
|
||||
marginRight: 12,
|
||||
},
|
||||
|
||||
settingsItemText: {
|
||||
fontSize: 16,
|
||||
color: 'var(--text-primary)',
|
||||
},
|
||||
|
||||
settingsItemValue: {
|
||||
fontSize: 15,
|
||||
color: 'var(--text-tertiary)',
|
||||
},
|
||||
|
||||
// Modal styles - Clean dialogs
|
||||
modalContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: 'var(--black-50)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
modalContent: {
|
||||
backgroundColor: 'var(--background-card)',
|
||||
borderRadius: 8,
|
||||
padding: 20,
|
||||
width: '80%',
|
||||
maxWidth: 300,
|
||||
},
|
||||
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: 'var(--text-primary)',
|
||||
marginBottom: 16,
|
||||
textAlign: 'center',
|
||||
},
|
||||
|
||||
modalInput: {
|
||||
borderWidth: 1,
|
||||
borderColor: 'var(--border)',
|
||||
borderRadius: 4,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
color: 'var(--text-primary)',
|
||||
marginBottom: 16,
|
||||
},
|
||||
|
||||
modalButtons: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
|
||||
modalButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 12,
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
},
|
||||
|
||||
modalButtonCancel: {
|
||||
backgroundColor: 'var(--background-secondary)',
|
||||
marginRight: 8,
|
||||
},
|
||||
|
||||
modalButtonConfirm: {
|
||||
backgroundColor: 'var(--primary)',
|
||||
marginLeft: 8,
|
||||
},
|
||||
|
||||
modalButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
||||
modalButtonTextCancel: {
|
||||
color: 'var(--text-primary)',
|
||||
},
|
||||
|
||||
modalButtonTextConfirm: {
|
||||
color: 'var(--text-inverted)',
|
||||
},
|
||||
|
||||
// Overlay style for dismissing folder list
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'transparent',
|
||||
zIndex: 99,
|
||||
},
|
||||
};
|
||||
21
src/utils/types.js
Normal file
21
src/utils/types.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// Types
|
||||
export const Note = {
|
||||
id: String,
|
||||
title: String,
|
||||
content: String,
|
||||
createdAt: Date,
|
||||
updatedAt: Date,
|
||||
folderId: String,
|
||||
isStarred: Boolean
|
||||
};
|
||||
|
||||
export const Folder = {
|
||||
id: String,
|
||||
name: String,
|
||||
createdAt: Date
|
||||
};
|
||||
|
||||
export const Settings = {
|
||||
cloudSync: Boolean,
|
||||
darkMode: Boolean
|
||||
};
|
||||
Reference in New Issue
Block a user