diff --git a/api/api.go b/api/api.go index 2983479..8836149 100644 --- a/api/api.go +++ b/api/api.go @@ -97,7 +97,11 @@ func (api *API) registerWebAppRoutes() { // Local / Offline Static Pages (No Template, No Auth) api.Router.GET("/local", api.localDocuments) + + // Reader (Reader Page, Document Progress, Devices) api.Router.GET("/reader", api.documentReader) + api.Router.GET("/reader/devices", api.authWebAppMiddleware, api.getDevices) + api.Router.GET("/reader/progress/:document", api.authWebAppMiddleware, api.getDocumentProgress) // Web App api.Router.GET("/", api.authWebAppMiddleware, api.createAppResourcesRoute("home")) @@ -106,7 +110,6 @@ func (api *API) registerWebAppRoutes() { api.Router.GET("/documents/:document", api.authWebAppMiddleware, api.createAppResourcesRoute("document")) api.Router.GET("/documents/:document/cover", api.authWebAppMiddleware, api.getDocumentCover) api.Router.GET("/documents/:document/file", api.authWebAppMiddleware, api.downloadDocument) - api.Router.GET("/documents/:document/progress", api.authWebAppMiddleware, api.getDocumentProgress) api.Router.GET("/login", api.createAppResourcesRoute("login")) api.Router.GET("/logout", api.authWebAppMiddleware, api.authLogout) api.Router.GET("/register", api.createAppResourcesRoute("login", gin.H{"Register": true})) diff --git a/api/app-routes.go b/api/app-routes.go index 4d9c772..beba89c 100644 --- a/api/app-routes.go +++ b/api/app-routes.go @@ -113,10 +113,9 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any } templateVars["User"] = userID - // Potential URL Parameters - qParams := bindQueryParams(c) - if routeName == "documents" { + qParams := bindQueryParams(c, 9) + var query *string if qParams.Search != nil && *qParams.Search != "" { search := "%" + *qParams.Search + "%" @@ -181,6 +180,8 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any templateVars["Data"] = document templateVars["TotalTimeLeftSeconds"] = int64((100.0 - document.Percentage) * float64(document.SecondsPerPercent)) } else if routeName == "activity" { + qParams := bindQueryParams(c, 15) + activityFilter := database.GetActivityParams{ UserID: userID, Offset: (*qParams.Page - 1) * *qParams.Limit, @@ -397,6 +398,20 @@ func (api *API) getDocumentProgress(c *gin.Context) { }) } +func (api *API) getDevices(c *gin.Context) { + rUser, _ := c.Get("AuthorizedUser") + + devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, rUser.(string)) + + if err != nil && err != sql.ErrNoRows { + log.Error("[getDevices] GetDevices DB Error:", err) + errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err)) + return + } + + c.JSON(http.StatusOK, devices) +} + func (api *API) uploadNewDocument(c *gin.Context) { var rDocUpload requestDocumentUpload if err := c.ShouldBind(&rDocUpload); err != nil { @@ -981,13 +996,12 @@ func (api *API) getDocumentsWordCount(documents []database.GetDocumentsWithStats return nil } -func bindQueryParams(c *gin.Context) queryParams { +func bindQueryParams(c *gin.Context, defaultLimit int64) queryParams { var qParams queryParams c.BindQuery(&qParams) if qParams.Limit == nil { - var defaultValue int64 = 9 - qParams.Limit = &defaultValue + qParams.Limit = &defaultLimit } else if *qParams.Limit < 0 { var zeroValue int64 = 0 qParams.Limit = &zeroValue diff --git a/api/opds-routes.go b/api/opds-routes.go index 655bacd..dd2935f 100644 --- a/api/opds-routes.go +++ b/api/opds-routes.go @@ -49,7 +49,7 @@ func (api *API) opdsEntry(c *gin.Context) { }, Links: []opds.Link{ { - Href: "/api/opds/documents?limit=100", + Href: "/api/opds/documents", TypeLink: "application/atom+xml;type=feed;profile=opds-catalog", }, }, @@ -66,8 +66,8 @@ func (api *API) opdsDocuments(c *gin.Context) { userID = rUser.(string) } - // Potential URL Parameters - qParams := bindQueryParams(c) + // Potential URL Parameters (Default Pagination - 100) + qParams := bindQueryParams(c, 100) // Possible Query var query *string @@ -160,7 +160,7 @@ func (api *API) opdsSearchDescription(c *gin.Context) { rawXML := ` Search AnthoLume Search AnthoLume - + ` c.Data(http.StatusOK, "application/xml", []byte(rawXML)) } diff --git a/assets/lib/platform.min.js b/assets/lib/platform.min.js deleted file mode 100644 index a74305a..0000000 --- a/assets/lib/platform.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(){"use strict";var e={function:!0,object:!0},G=e[typeof window]&&window||this,i=e[typeof exports]&&exports,t=e[typeof module]&&module&&!module.nodeType&&module,r=i&&t&&"object"==typeof global&&global;!r||r.global!==r&&r.window!==r&&r.self!==r||(G=r);var a=Math.pow(2,53)-1,$=/\bOpera/,n=Object.prototype,o=n.hasOwnProperty,X=n.toString;function l(e){return(e=String(e)).charAt(0).toUpperCase()+e.slice(1)}function K(e){return e=H(e),/^(?:webOS|i(?:OS|P))/.test(e)?e:l(e)}function j(e,t){for(var i in e)o.call(e,i)&&t(e[i],i,e)}function N(e){return null==e?l(e):X.call(e).slice(8,-1)}function V(e){return String(e).replace(/([ -])(?!$)/g,"$1?")}function z(i,r){var n=null;return function(e,t){var i=-1,r=e?e.length:0;if("number"==typeof r&&-1 - diff --git a/assets/reader/index.html b/assets/reader/index.html index 8ec5567..c593eae 100644 --- a/assets/reader/index.html +++ b/assets/reader/index.html @@ -20,7 +20,6 @@ - @@ -71,6 +70,10 @@ #top-bar:not(.top-0) { top: calc((8em + env(safe-area-inset-top)) * -1); } + + select:invalid { + color: gray; + } @@ -260,5 +263,120 @@
+ + + diff --git a/assets/reader/index.js b/assets/reader/index.js index 8035ec8..3ae2173 100644 --- a/assets/reader/index.js +++ b/assets/reader/index.js @@ -17,7 +17,7 @@ async function initReader() { if (documentType == "REMOTE") { // Get Server / Cached Document - let progressResp = await fetch("/documents/" + documentID + "/progress"); + let progressResp = await fetch("/reader/progress/" + documentID); documentData = await progressResp.json(); // Update With Local Cache @@ -145,14 +145,60 @@ class EBookReader { ); } - this.readerSettings.deviceName = - this.readerSettings.deviceName || - platform.os.toString() + " - " + platform.name; + // Device Already Set + if (this.readerSettings.deviceID) return; - this.readerSettings.deviceID = this.readerSettings.deviceID || randomID(); + // Get Elements + let devicePopup = document.querySelector("#device-selector"); + let devSelector = devicePopup.querySelector("select"); + let devInput = devicePopup.querySelector("input"); + let [assumeButton, createButton] = devicePopup.querySelectorAll("button"); - // Save Settings (Device ID) - this.saveSettings(); + // Set Visible + devicePopup.classList.remove("hidden"); + + // Add Devices + fetch("/reader/devices").then(async (r) => { + let data = await r.json(); + + data.forEach((item) => { + let optionEl = document.createElement("option"); + optionEl.value = item.id; + optionEl.textContent = item.device_name; + devSelector.appendChild(optionEl); + }); + }); + + assumeButton.addEventListener("click", () => { + let deviceID = devSelector.value; + + if (deviceID == "") { + // TODO - Error Message + return; + } + + let selectedOption = devSelector.children[devSelector.selectedIndex]; + let deviceName = selectedOption.textContent; + + this.readerSettings.deviceID = deviceID; + this.readerSettings.deviceName = deviceName; + this.saveSettings(); + devicePopup.classList.add("hidden"); + }); + + createButton.addEventListener("click", () => { + let deviceName = devInput.value.trim(); + + if (deviceName == "") { + // TODO - Error Message + return; + } + + this.readerSettings.deviceID = randomID(); + this.readerSettings.deviceName = deviceName; + this.saveSettings(); + devicePopup.classList.add("hidden"); + }); } /** diff --git a/assets/sw.js b/assets/sw.js index 25472fa..fb30f44 100644 --- a/assets/sw.js +++ b/assets/sw.js @@ -44,7 +44,7 @@ const ROUTES = [ type: CACHE_UPDATE_ASYNC, }, { - route: /^\/documents\/[a-zA-Z0-9]{32}\/progress$/, + route: /^\/reader\/progress\/[a-zA-Z0-9]{32}$/, type: CACHE_UPDATE_SYNC, }, { @@ -74,7 +74,6 @@ const PRECACHE_ASSETS = [ "/assets/common.js", // Library Assets - "/assets/lib/platform.min.js", "/assets/lib/jszip.min.js", "/assets/lib/epub.min.js", "/assets/lib/no-sleep.min.js", @@ -120,7 +119,9 @@ async function handleFetch(event) { // Find Directive const directive = ROUTES.find( - (item) => url.match(item.route) || url == item.route + (item) => + (item.route instanceof RegExp && url.match(item.route)) || + url == item.route ) || { type: CACHE_NEVER }; // Get Fallback @@ -170,12 +171,22 @@ function handleMessage(event) { caches.open(SW_CACHE_NAME).then(async (cache) => { let allKeys = await cache.keys(); + // Get Cached Resources let docResources = allKeys .map((item) => new URL(item.url).pathname) - .filter((item) => item.startsWith("/documents/")); + .filter( + (item) => + item.startsWith("/documents/") || + item.startsWith("/reader/progress/") + ); + // Derive Unique IDs let documentIDs = Array.from( - new Set(docResources.map((item) => item.split("/")[2])) + new Set( + docResources + .filter((item) => item.startsWith("/documents/")) + .map((item) => item.split("/")[2]) + ) ); /** @@ -188,10 +199,10 @@ function handleMessage(event) { .filter( (id) => docResources.includes("/documents/" + id + "/file") && - docResources.includes("/documents/" + id + "/progress") + docResources.includes("/reader/progress/" + id) ) .map(async (id) => { - let url = "/documents/" + id + "/progress"; + let url = "/reader/progress/" + id; let currentCache = await caches.match(url); let resp = await updateCache(url).catch((e) => currentCache); return resp.json(); @@ -201,13 +212,12 @@ function handleMessage(event) { event.source.postMessage({ id, data: cachedDocuments }); }); } else if (data.type === DEL_SW_CACHE) { - let basePath = "/documents/" + data.id; caches .open(SW_CACHE_NAME) .then((cache) => Promise.all([ - cache.delete(basePath + "/file"), - cache.delete(basePath + "/progress"), + cache.delete("/documents/" + data.id + "/file"), + cache.delete("/reader/progress/" + data.id), ]) ) .then(() => event.source.postMessage({ id, data: "SUCCESS" })) diff --git a/database/query.sql b/database/query.sql index 16c7b0d..6ed229f 100644 --- a/database/query.sql +++ b/database/query.sql @@ -40,9 +40,12 @@ WHERE id = $id; WITH filtered_activity AS ( SELECT document_id, + device_id, user_id, start_time, duration, + ROUND(CAST(start_percentage AS REAL) * 100, 2) AS start_percentage, + ROUND(CAST(end_percentage AS REAL) * 100, 2) AS end_percentage, ROUND(CAST(end_percentage - start_percentage AS REAL) * 100, 2) AS read_percentage FROM activity WHERE @@ -60,10 +63,13 @@ WITH filtered_activity AS ( SELECT document_id, + device_id, CAST(STRFTIME('%Y-%m-%d %H:%M:%S', activity.start_time, users.time_offset) AS TEXT) AS start_time, title, author, duration, + start_percentage, + end_percentage, read_percentage FROM filtered_activity AS activity LEFT JOIN documents ON documents.id = activity.document_id @@ -128,6 +134,7 @@ WHERE id = $device_id LIMIT 1; -- name: GetDevices :many SELECT + devices.id, devices.device_name, CAST(STRFTIME('%Y-%m-%d %H:%M:%S', devices.created_at, users.time_offset) AS TEXT) AS created_at, CAST(STRFTIME('%Y-%m-%d %H:%M:%S', devices.last_synced, users.time_offset) AS TEXT) AS last_synced diff --git a/database/query.sql.go b/database/query.sql.go index 5f2bcd6..a5b9c8b 100644 --- a/database/query.sql.go +++ b/database/query.sql.go @@ -150,9 +150,12 @@ const getActivity = `-- name: GetActivity :many WITH filtered_activity AS ( SELECT document_id, + device_id, user_id, start_time, duration, + ROUND(CAST(start_percentage AS REAL) * 100, 2) AS start_percentage, + ROUND(CAST(end_percentage AS REAL) * 100, 2) AS end_percentage, ROUND(CAST(end_percentage - start_percentage AS REAL) * 100, 2) AS read_percentage FROM activity WHERE @@ -170,10 +173,13 @@ WITH filtered_activity AS ( SELECT document_id, + device_id, CAST(STRFTIME('%Y-%m-%d %H:%M:%S', activity.start_time, users.time_offset) AS TEXT) AS start_time, title, author, duration, + start_percentage, + end_percentage, read_percentage FROM filtered_activity AS activity LEFT JOIN documents ON documents.id = activity.document_id @@ -189,12 +195,15 @@ type GetActivityParams struct { } type GetActivityRow struct { - DocumentID string `json:"document_id"` - StartTime string `json:"start_time"` - Title *string `json:"title"` - Author *string `json:"author"` - Duration int64 `json:"duration"` - ReadPercentage float64 `json:"read_percentage"` + DocumentID string `json:"document_id"` + DeviceID string `json:"device_id"` + StartTime string `json:"start_time"` + Title *string `json:"title"` + Author *string `json:"author"` + Duration int64 `json:"duration"` + StartPercentage float64 `json:"start_percentage"` + EndPercentage float64 `json:"end_percentage"` + ReadPercentage float64 `json:"read_percentage"` } func (q *Queries) GetActivity(ctx context.Context, arg GetActivityParams) ([]GetActivityRow, error) { @@ -214,10 +223,13 @@ func (q *Queries) GetActivity(ctx context.Context, arg GetActivityParams) ([]Get var i GetActivityRow if err := rows.Scan( &i.DocumentID, + &i.DeviceID, &i.StartTime, &i.Title, &i.Author, &i.Duration, + &i.StartPercentage, + &i.EndPercentage, &i.ReadPercentage, ); err != nil { return nil, err @@ -390,6 +402,7 @@ func (q *Queries) GetDevice(ctx context.Context, deviceID string) (Device, error const getDevices = `-- name: GetDevices :many SELECT + devices.id, devices.device_name, CAST(STRFTIME('%Y-%m-%d %H:%M:%S', devices.created_at, users.time_offset) AS TEXT) AS created_at, CAST(STRFTIME('%Y-%m-%d %H:%M:%S', devices.last_synced, users.time_offset) AS TEXT) AS last_synced @@ -400,6 +413,7 @@ ORDER BY devices.last_synced DESC ` type GetDevicesRow struct { + ID string `json:"id"` DeviceName string `json:"device_name"` CreatedAt string `json:"created_at"` LastSynced string `json:"last_synced"` @@ -414,7 +428,12 @@ func (q *Queries) GetDevices(ctx context.Context, userID string) ([]GetDevicesRo var items []GetDevicesRow for rows.Next() { var i GetDevicesRow - if err := rows.Scan(&i.DeviceName, &i.CreatedAt, &i.LastSynced); err != nil { + if err := rows.Scan( + &i.ID, + &i.DeviceName, + &i.CreatedAt, + &i.LastSynced, + ); err != nil { return nil, err } items = append(items, i) diff --git a/templates/activity.html b/templates/activity.html index 1d7c055..e0aec2e 100644 --- a/templates/activity.html +++ b/templates/activity.html @@ -38,7 +38,7 @@

{{ $activity.Duration }}

-

{{ $activity.ReadPercentage }}%

+

{{ $activity.EndPercentage }}%

{{end}} diff --git a/templates/documents.html b/templates/documents.html index f2b8aab..1e060fc 100644 --- a/templates/documents.html +++ b/templates/documents.html @@ -35,7 +35,7 @@ type="text" id="search" name="search" - class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent" + class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent" placeholder="Search Author / Title" />