Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions Readmigo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Readmigo/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
// Let Firebase Auth handle phone auth verification notifications
guard FirebaseApp.app() != nil else {
completionHandler(.noData)
return
}
if Auth.auth().canHandleNotification(userInfo) {
completionHandler(.noData)
return
Expand All @@ -81,6 +85,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
// Forward to Firebase Auth for phone auth verification
guard FirebaseApp.app() != nil else { return }
Auth.auth().setAPNSToken(deviceToken, type: .unknown)

let token = deviceToken.map { String(format: "%02x", $0) }.joined()
Expand Down
14 changes: 12 additions & 2 deletions Readmigo/App/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,24 @@ struct MainTabView: View {
}
.tag(0)

BookshelfView()
.environmentObject(libraryManager)
.tabItem {
Label("tab.bookshelf".localized, systemImage: "books.vertical")
}
.tag(1)

AudiobookListView()
.tabItem {
Label("tab.audiobook".localized, systemImage: "headphones")
}
.tag(1)
.tag(2)

MeView()
.tabItem {
Label("tab.me".localized, systemImage: "person.circle")
}
.tag(2)
.tag(3)
.badge(notificationService.unreadCount)
}

Expand Down Expand Up @@ -125,6 +132,8 @@ struct MainTabView: View {
case 0:
NotificationCenter.default.post(name: .bookstoreTabDoubleTapped, object: nil)
case 1:
NotificationCenter.default.post(name: .bookshelfTabDoubleTapped, object: nil)
case 2:
NotificationCenter.default.post(name: .audiobookTabDoubleTapped, object: nil)
default:
break
Expand Down Expand Up @@ -211,6 +220,7 @@ struct MiniPlayerBar: View {

extension Notification.Name {
static let bookstoreTabDoubleTapped = Notification.Name("bookstoreTabDoubleTapped")
static let bookshelfTabDoubleTapped = Notification.Name("bookshelfTabDoubleTapped")
static let audiobookTabDoubleTapped = Notification.Name("audiobookTabDoubleTapped")
static let agoraTabDoubleTapped = Notification.Name("agoraTabDoubleTapped")
static let fullScreenCoverPresented = Notification.Name("fullScreenCoverPresented")
Expand Down
154 changes: 154 additions & 0 deletions Readmigo/Core/Database/DAO/BookshelfDAO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import GRDB
import Foundation

struct BookshelfDAO {
let dbPool: DatabasePool

// MARK: - Folders

func getAllFolders() throws -> [BookshelfFolderRecord] {
try dbPool.read { db in
try BookshelfFolderRecord
.order(Column("sort_order").asc)
.fetchAll(db)
}
}

func getFolder(id: String) throws -> BookshelfFolderRecord? {
try dbPool.read { db in
try BookshelfFolderRecord.fetchOne(db, key: id)
}
}

func upsertFolder(_ record: BookshelfFolderRecord) throws {
try dbPool.write { db in
try record.save(db)
}
}

func deleteFolder(id: String) throws {
try dbPool.write { db in
// Delete all items in this folder first
try BookshelfItemRecord
.filter(Column("folder_id") == id)
.deleteAll(db)
// Delete the folder
_ = try BookshelfFolderRecord.deleteOne(db, key: id)
}
}

func getMaxFolderSortOrder() throws -> Int {
try dbPool.read { db in
let row = try Row.fetchOne(db, sql: "SELECT MAX(sort_order) FROM bookshelf_folders")
return row?[0] as? Int ?? -1
}
}

// MARK: - Items

func getAllItems() throws -> [BookshelfItemRecord] {
try dbPool.read { db in
try BookshelfItemRecord
.order(Column("sort_order").asc)
.fetchAll(db)
}
}

func getItems(folderId: String?) throws -> [BookshelfItemRecord] {
try dbPool.read { db in
if let folderId = folderId {
return try BookshelfItemRecord
.filter(Column("folder_id") == folderId)
.order(Column("sort_order").asc)
.fetchAll(db)
} else {
return try BookshelfItemRecord
.filter(Column("folder_id") == nil)
.order(Column("sort_order").asc)
.fetchAll(db)
}
}
}

func getItem(bookId: String) throws -> BookshelfItemRecord? {
try dbPool.read { db in
try BookshelfItemRecord
.filter(Column("book_id") == bookId)
.fetchOne(db)
}
}

func upsertItem(_ record: BookshelfItemRecord) throws {
try dbPool.write { db in
try record.save(db)
}
}

func deleteItem(id: String) throws {
try dbPool.write { db in
_ = try BookshelfItemRecord.deleteOne(db, key: id)
}
}

func deleteItem(bookId: String) throws {
try dbPool.write { db in
try BookshelfItemRecord
.filter(Column("book_id") == bookId)
.deleteAll(db)
}
}

func getMaxItemSortOrder(folderId: String?) throws -> Int {
try dbPool.read { db in
if let folderId = folderId {
let row = try Row.fetchOne(db, sql: "SELECT MAX(sort_order) FROM bookshelf_items WHERE folder_id = ?", arguments: [folderId])
return row?[0] as? Int ?? -1
} else {
let row = try Row.fetchOne(db, sql: "SELECT MAX(sort_order) FROM bookshelf_items WHERE folder_id IS NULL")
return row?[0] as? Int ?? -1
}
}
}

func moveItem(bookId: String, toFolderId: String?) throws {
try dbPool.write { db in
guard var item = try BookshelfItemRecord
.filter(Column("book_id") == bookId)
.fetchOne(db) else { return }

item.folderId = toFolderId

// Get max sort order in destination
let maxOrder: Int
if let folderId = toFolderId {
let row = try Row.fetchOne(db, sql: "SELECT MAX(sort_order) FROM bookshelf_items WHERE folder_id = ?", arguments: [folderId])
maxOrder = (row?[0] as? Int ?? -1) + 1
} else {
let row = try Row.fetchOne(db, sql: "SELECT MAX(sort_order) FROM bookshelf_items WHERE folder_id IS NULL")
maxOrder = (row?[0] as? Int ?? -1) + 1
}
item.sortOrder = maxOrder

try item.save(db)
}
}

func itemCount(folderId: String?) throws -> Int {
try dbPool.read { db in
if let folderId = folderId {
return try BookshelfItemRecord
.filter(Column("folder_id") == folderId)
.fetchCount(db)
} else {
return try BookshelfItemRecord.fetchCount(db)
}
}
}

func deleteAll() throws {
try dbPool.write { db in
try BookshelfItemRecord.deleteAll(db)
try BookshelfFolderRecord.deleteAll(db)
}
}
}
24 changes: 24 additions & 0 deletions Readmigo/Core/Database/Migrations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -255,5 +255,29 @@ enum Migrations {
t.add(column: "paragraph_index", .integer).notNull().defaults(to: 0)
}
}

migrator.registerMigration("v5_bookshelf") { db in
try db.create(table: "bookshelf_folders") { t in
t.primaryKey("id", .text)
t.column("name", .text).notNull()
t.column("icon", .text)
t.column("color", .text)
t.column("sort_order", .integer).notNull().defaults(to: 0)
t.column("created_at", .integer).notNull()
t.column("updated_at", .integer).notNull()
}
try db.create(index: "idx_bookshelf_folders_order", on: "bookshelf_folders", columns: ["sort_order"])

try db.create(table: "bookshelf_items") { t in
t.primaryKey("id", .text)
t.column("folder_id", .text)
.references("bookshelf_folders", onDelete: .cascade)
t.column("book_id", .text).notNull()
t.column("sort_order", .integer).notNull().defaults(to: 0)
t.column("added_at", .integer).notNull()
}
try db.create(index: "idx_bookshelf_items_folder", on: "bookshelf_items", columns: ["folder_id"])
try db.create(index: "idx_bookshelf_items_book", on: "bookshelf_items", columns: ["book_id"], unique: true)
}
}
}
45 changes: 45 additions & 0 deletions Readmigo/Core/Database/Records/BookshelfRecord.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import GRDB
import Foundation

// MARK: - Bookshelf Folder Record

struct BookshelfFolderRecord: Codable, FetchableRecord, PersistableRecord {
static let databaseTableName = "bookshelf_folders"
static let persistenceConflictPolicy = PersistenceConflictPolicy(insert: .replace, update: .replace)

var id: String
var name: String
var icon: String?
var color: String?
var sortOrder: Int
var createdAt: Int64
var updatedAt: Int64

enum CodingKeys: String, CodingKey {
case id, name, icon, color
case sortOrder = "sort_order"
case createdAt = "created_at"
case updatedAt = "updated_at"
}
}

// MARK: - Bookshelf Item Record

struct BookshelfItemRecord: Codable, FetchableRecord, PersistableRecord {
static let databaseTableName = "bookshelf_items"
static let persistenceConflictPolicy = PersistenceConflictPolicy(insert: .replace, update: .replace)

var id: String
var folderId: String?
var bookId: String
var sortOrder: Int
var addedAt: Int64

enum CodingKeys: String, CodingKey {
case id
case folderId = "folder_id"
case bookId = "book_id"
case sortOrder = "sort_order"
case addedAt = "added_at"
}
}
Loading
Loading