mirror of
https://github.com/Tony0410/readlater.git
synced 2026-05-24 22:01:41 +08:00
Extract publication dates from HTML meta tags when saving articles and display them prominently in the article list and reader header. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
117 lines
4.9 KiB
TypeScript
117 lines
4.9 KiB
TypeScript
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(),
|
|
title: text("title").notNull(),
|
|
author: text("author"),
|
|
siteName: text("site_name"),
|
|
excerpt: text("excerpt"),
|
|
content: text("content").notNull(), // HTML content
|
|
textContent: text("text_content").notNull(), // Plain text for TTS
|
|
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
|
|
publishedAt: integer("published_at", { mode: "timestamp" }), // Original article publish date
|
|
});
|
|
|
|
// 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;
|