diff --git a/Podfile b/Podfile index a4c28aa..49ce6fa 100644 --- a/Podfile +++ b/Podfile @@ -7,7 +7,7 @@ target 'SQLiteViewer' do use_frameworks! # Pods for SQLiteViewer - pod 'Http.swift', '~> 2.1' + pod 'Http.swift', '~> 2.2.1' pod 'SQLite.swift', '~> 0.11.5' target 'SQLiteViewerTests' do diff --git a/SQLite.viewer.podspec b/SQLite.viewer.podspec index 3549237..7cd98ef 100644 --- a/SQLite.viewer.podspec +++ b/SQLite.viewer.podspec @@ -12,6 +12,6 @@ Pod::Spec.new do |s| s.source_files = 'Sources/*.swift' s.resource_bundles = { 'com.biatoms.sqlite-viewer.assets' => ['Sources/**/*.{js,css,ico,html}'] } - s.dependency 'Http.swift', '~> 2.1.1' + s.dependency 'Http.swift', '~> 2.2.1' s.dependency 'SQLite.swift', '>= 0.11.5' end diff --git a/Sources/Assets/css/app.css b/Sources/Assets/css/app.css new file mode 100644 index 0000000..479e8a5 --- /dev/null +++ b/Sources/Assets/css/app.css @@ -0,0 +1,62 @@ +#query-container { + position: fixed; + left: 0px; + top: 0px; + width: 100%; + height: 50px; + z-index: 2; + background: white; +} +#query-container>div { + display: flex; + vertical-align: middle; +} +#query-container>div>textarea { + flex-grow: 1; + min-width: 50%; +} +.row { + margin: 50px 0 0; + display: grid; + grid-template-columns: 1fr 1fr 8fr; + gap: 10px; +} +.row>div.databases { + grid-area: 1/1/1/1; +} +.row>div.tables { + grid-area: 1/2/1/2; +} +.row>div.data { + grid-area: 1/3/1/3; +} +table thead { + position: sticky; + top: 50px; + z-index: 1; +} +table thead td, table thead th { + background: white; +} +table tr td { + position: relative; +} +table tr td .edit-item { + opacity: 0; + position: absolute; + top: 0px; + right: 0px; +} +table tr:hover td .edit-item { + opacity: 1; +} +table tbody td span { + display: inline-block; +} +table tbody td.is-long span { + width: 50vw; + word-break: break-all; +} +table tbody td.is-null { + color: gray; +} diff --git a/Sources/Assets/index.html b/Sources/Assets/index.html index f188d35..e1b5415 100755 --- a/Sources/Assets/index.html +++ b/Sources/Assets/index.html @@ -8,28 +8,26 @@ + + -
+
-
- -
+
-
- -
-
+
Databases
+
-
+
Tables
@@ -37,7 +35,7 @@
-
+
Data
@@ -46,6 +44,5 @@
- diff --git a/Sources/Assets/js/app.js b/Sources/Assets/js/app.js index c505719..ab6ce90 100644 --- a/Sources/Assets/js/app.js +++ b/Sources/Assets/js/app.js @@ -57,11 +57,11 @@ function refreshTableList() { function refreshTableData() { $('#table-data').empty(); Api.getTableData(currentDatabase, currentTable, function (data) { - displayTable(data); + displayTable(data, currentTable); }) } -function displayTable(data) { +function displayTable(data, tablename) { var elem = $('#table-data'); elem.empty(); var table = $(''); @@ -78,28 +78,51 @@ function displayTable(data) { data.rows.forEach(function (row) { var rowElem = $(''); tableBody.append(rowElem); - row.forEach(function (data) { - rowElem.append($('
').text(data)); + row.forEach(function (value, index) { + let origValue = value; + if (data.blobs.indexOf(index) !== -1 && value) { + value = 'blob: ' + value; + } + let cell = $('').append($('').text(value ?? 'null')); + if (value == null) { + cell.addClass('is-null'); + } else if (value?.length > 200) { + cell.addClass('is-long'); + } + rowElem.append(cell); + if (tablename && data.columns[0] === 'Z_PK') { + // This is a CoreData helper + cell.append($('').addClass('edit-item').text('edit').attr('href', '#').on('click', function(e) { + $('#query').val('UPDATE '+tablename+' SET '+data.columns[index]+' = \''+escapeSqlite(origValue)+'\' WHERE '+data.columns[0]+' = \''+escapeSqlite(row[0])+'\''); + message('SQL updated'); + e.preventDefault(true); + e.stopPropagation(true); + return false; + })); + } }) }); } - +function escapeSqlite(string) { + return (''+string).replace(/'/g, '\'\''); +} function executeQuery(query) { Api.executeQuery(currentDatabase, query, function (data) { if (data.hasOwnProperty('affected_rows')) { - message(data.affected_rows + " rows are affected."); + message(data.affected_rows + ' rows are affected.'); } else { displayTable(data); - message("Query completed."); + message('Query completed.'); } }); } function download() { Api.downloadDatabase(currentDatabase, function() { - message(currentDatabase + " is downloaded.") + message(currentDatabase + ' is downloaded.'); }); } - -init(); +$(document).ready(function () { + init(); +}) diff --git a/Sources/DatabaseController.swift b/Sources/DatabaseController.swift index 889add1..3ef2459 100644 --- a/Sources/DatabaseController.swift +++ b/Sources/DatabaseController.swift @@ -70,10 +70,24 @@ open class DatabaseController { func toArray(_ elements: S) -> Array { // resolves ambiguity return Array(elements) } - + var blobs: [Int] = [] + var rows = toArray(statement) + if !JSONSerialization.isValidJSONObject(rows) { + for (rowIndex, row) in rows.enumerated() { + for (cellIndex, cell) in row.enumerated() { + if let cellBlob = cell as? SQLite.Blob { + if !blobs.contains(cellIndex) { + blobs.append(cellIndex) + } + rows[rowIndex][cellIndex] = Data(cellBlob.bytes).base64EncodedString() + } + } + } + } return [ "columns": statement.columnNames, - "rows": toArray(statement) + "blobs": blobs, + "rows": rows ] } } diff --git a/Sources/SQLiteViewer.swift b/Sources/SQLiteViewer.swift index 909a76e..e6dfdcb 100644 --- a/Sources/SQLiteViewer.swift +++ b/Sources/SQLiteViewer.swift @@ -10,7 +10,7 @@ import HttpSwift import SQLite open class SQLiteViewer { - open static var shared = SQLiteViewer() + public static var shared = SQLiteViewer() public var assetDir: String = "" public var dbDir: String = "" {