mirror of
https://github.com/Tony0410/readlater.git
synced 2026-05-25 06:11:40 +08:00
v2.0: Major feature update
New Features: - API key authentication for external access - Apple Shortcuts integration endpoint (/api/v1/add) - Full-text search across all articles - Folders for organizing articles - Highlights and notes on articles - Reading stats with streaks - Reading goals (daily/weekly/monthly) - Import from Pocket/Instapaper - RSS feed output - PWA support for mobile - Auto theme scheduling (day/night) - Settings page with all configuration API Endpoints: - /api/v1/add - Add articles via API key - /api/keys - Manage API keys - /api/search - Full-text search - /api/folders - Folder management - /api/highlights - Highlights/notes - /api/stats - Reading statistics - /api/goals - Reading goals - /api/import - Pocket/Instapaper import - /api/rss - RSS feed Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,26 @@
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
||||
import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core";
|
||||
|
||||
// API Keys for authentication
|
||||
export const apiKeys = sqliteTable("api_keys", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(), // e.g., "iPhone Shortcut", "Mac Safari"
|
||||
key: text("key").notNull().unique(),
|
||||
lastUsed: integer("last_used", { mode: "timestamp" }),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
// Folders for organizing articles
|
||||
export const folders = sqliteTable("folders", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
color: text("color").default("#3b82f6"), // Accent color
|
||||
icon: text("icon").default("folder"), // Lucide icon name
|
||||
parentId: text("parent_id"), // For nested folders
|
||||
sortOrder: integer("sort_order").default(0),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
// Articles
|
||||
export const articles = sqliteTable("articles", {
|
||||
id: text("id").primaryKey(),
|
||||
url: text("url").notNull(),
|
||||
@@ -12,13 +33,83 @@ export const articles = sqliteTable("articles", {
|
||||
leadImage: text("lead_image"),
|
||||
wordCount: integer("word_count").default(0),
|
||||
readingProgress: integer("reading_progress").default(0), // 0-100
|
||||
readingTimeSeconds: integer("reading_time_seconds").default(0), // Total time spent reading
|
||||
isFavorite: integer("is_favorite", { mode: "boolean" }).default(false),
|
||||
isArchived: integer("is_archived", { mode: "boolean" }).default(false),
|
||||
folderId: text("folder_id"), // Link to folder
|
||||
tags: text("tags").default("[]"), // JSON array of tags
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
||||
readAt: integer("read_at", { mode: "timestamp" }),
|
||||
finishedAt: integer("finished_at", { mode: "timestamp" }), // When reading was completed
|
||||
});
|
||||
|
||||
// Highlights and notes
|
||||
export const highlights = sqliteTable("highlights", {
|
||||
id: text("id").primaryKey(),
|
||||
articleId: text("article_id").notNull(),
|
||||
text: text("text").notNull(), // Highlighted text
|
||||
note: text("note"), // Optional note
|
||||
color: text("color").default("#fbbf24"), // Highlight color
|
||||
startOffset: integer("start_offset"), // Position in textContent
|
||||
endOffset: integer("end_offset"),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
// Daily reading stats
|
||||
export const readingStats = sqliteTable("reading_stats", {
|
||||
id: text("id").primaryKey(),
|
||||
date: text("date").notNull().unique(), // YYYY-MM-DD format
|
||||
articlesRead: integer("articles_read").default(0),
|
||||
articlesAdded: integer("articles_added").default(0),
|
||||
wordsRead: integer("words_read").default(0),
|
||||
timeSpentSeconds: integer("time_spent_seconds").default(0),
|
||||
streak: integer("streak").default(0), // Consecutive days
|
||||
});
|
||||
|
||||
// Reading goals
|
||||
export const readingGoals = sqliteTable("reading_goals", {
|
||||
id: text("id").primaryKey(),
|
||||
type: text("type").notNull(), // "daily", "weekly", "monthly"
|
||||
metric: text("metric").notNull(), // "articles", "words", "time"
|
||||
target: integer("target").notNull(),
|
||||
isActive: integer("is_active", { mode: "boolean" }).default(true),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
// App settings
|
||||
export const settings = sqliteTable("settings", {
|
||||
key: text("key").primaryKey(),
|
||||
value: text("value").notNull(), // JSON encoded value
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
// Email inbox config (for email-to-save)
|
||||
export const emailConfig = sqliteTable("email_config", {
|
||||
id: text("id").primaryKey(),
|
||||
inboxEmail: text("inbox_email").unique(), // e.g., save-abc123@readlater.example.com
|
||||
isActive: integer("is_active", { mode: "boolean" }).default(true),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
// Types
|
||||
export type ApiKey = typeof apiKeys.$inferSelect;
|
||||
export type NewApiKey = typeof apiKeys.$inferInsert;
|
||||
|
||||
export type Folder = typeof folders.$inferSelect;
|
||||
export type NewFolder = typeof folders.$inferInsert;
|
||||
|
||||
export type Article = typeof articles.$inferSelect;
|
||||
export type NewArticle = typeof articles.$inferInsert;
|
||||
|
||||
export type Highlight = typeof highlights.$inferSelect;
|
||||
export type NewHighlight = typeof highlights.$inferInsert;
|
||||
|
||||
export type ReadingStats = typeof readingStats.$inferSelect;
|
||||
export type NewReadingStats = typeof readingStats.$inferInsert;
|
||||
|
||||
export type ReadingGoal = typeof readingGoals.$inferSelect;
|
||||
export type NewReadingGoal = typeof readingGoals.$inferInsert;
|
||||
|
||||
export type Setting = typeof settings.$inferSelect;
|
||||
export type NewSetting = typeof settings.$inferInsert;
|
||||
|
||||
Reference in New Issue
Block a user