Skip to content

Commit

Permalink
Add support for importing subscriptions, playlists, & history
Browse files Browse the repository at this point in the history
  • Loading branch information
MarmadileManteater committed Feb 5, 2024
1 parent 929010f commit 7a91081
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 14 deletions.
8 changes: 5 additions & 3 deletions src/renderer/components/data-settings/data-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,10 @@ export default defineComponent({
response.filePaths.forEach(filePath => {
if (filePath.endsWith('.csv')) {
this.importCsvYouTubeSubscriptions(textDecode)
} else if (filePath.endsWith('.db')) {
// opml and db are the same mime type 🤷‍♀️
} else if ((textDecode.trim().startsWith('{') && filePath.endsWith('.opml')) || filePath.endsWith('.db')) {
this.importFreeTubeSubscriptions(textDecode)
} else if (filePath.endsWith('.opml') || filePath.endsWith('.xml')) {
} else if (textDecode.trim().startsWith('<') && (filePath.endsWith('.opml') || filePath.endsWith('.xml'))) {
this.importOpmlYouTubeSubscriptions(textDecode)
} else if (filePath.endsWith('.json')) {
textDecode = JSON.parse(textDecode)
Expand Down Expand Up @@ -690,7 +691,8 @@ export default defineComponent({
}

response.filePaths.forEach(filePath => {
if (filePath.endsWith('.db')) {
// db and opml are the same mime type in android
if (filePath.endsWith('.db') || filePath.endsWith('.opml')) {
this.importFreeTubeHistory(textDecode.split('\n'))
} else if (filePath.endsWith('.json')) {
this.importYouTubeHistory(JSON.parse(textDecode))
Expand Down
70 changes: 60 additions & 10 deletions src/renderer/helpers/android.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@ import android from 'android'

export const STATE_PLAYING = 3
export const STATE_PAUSED = 2
export const MIME_TYPES = {
db: 'application/octet-stream',
json: 'application/json',
csv: 'text/comma-separated-values',
opml: 'application/octet-stream',
xml: 'text/xml'
}
export const FILE_TYPES = Object.fromEntries(Object.entries(MIME_TYPES).map(([key, value]) => [value, key]))

/**
* @typedef SaveDialogResponse
* @property {boolean} canceled
* @property {'SUCCESS'|'USER_CANCELED'} type
* @property {string?} uri
* @property {string[]} filePaths
*/

/**
Expand All @@ -32,32 +41,61 @@ export function updateMediaSessionState(state, position = null) {
}

/**
* Requests a save with a dialog
* @param {string} fileName name of requested file
* @param {string} fileType mime type
* Handles the response of a `requestDialog` function from the bridge
* @param {string} promiseId
* @returns {Promise<SaveDialogResponse>} either a uri based on the user's input or a cancelled response
*/
export async function requestSaveDialog(fileName, fileType) {
// request a 💾save dialog
const promiseId = android.requestSaveDialog(fileName, fileType)
async function handleDialogResponse(promiseId) {
// await the promise returned from the ☕ bridge
const response = await window.awaitAsyncResult(promiseId)
let response = await window.awaitAsyncResult(promiseId)
// handle case if user cancels prompt
if (response === 'USER_CANCELED') {
return {
canceled: true,
type: response,
uri: null
type: null,
uri: null,
filePaths: []
}
} else {
response = JSON.parse(response)
let typedUri = response?.uri
if (response?.type in FILE_TYPES && typedUri.indexOf('.') === -1) {
typedUri = `${typedUri}.${FILE_TYPES[response?.type]}`
}
return {
canceled: false,
type: 'SUCCESS',
uri: response
uri: response.uri,
filePaths: [typedUri]
}
}
}

/**
* Requests a save file dialog
* @param {string} fileName name of requested file
* @param {string} fileType mime type
* @returns {Promise<SaveDialogResponse>} either a uri based on the user's input or a cancelled response
*/
export function requestSaveDialog(fileName, fileType) {
// request a 💾save dialog
const promiseId = android.requestSaveDialog(fileName, fileType)
return handleDialogResponse(promiseId)
}

/**
* Requests an open file dialog
* @param {string[]} fileType mime type of acceptable inputs
* @returns {Promise<SaveDialogResponse>} either a uri based on the user's input or a cancelled response
*/
export function requestOpenDialog(fileTypes) {
const types = Array.from(new Set(fileTypes.map((type) => type in MIME_TYPES ? MIME_TYPES[type] : type)))

// request a 🗄file open dialog
const promiseId = android.requestOpenDialog(types.join(','))
return handleDialogResponse(promiseId)
}

/**
* @param {string} arg1 base uri or path
* @param {string} arg2 path or content
Expand All @@ -77,3 +115,15 @@ export function writeFile(arg1, arg2, arg3 = undefined) {
}
return android.writeFile(baseUri, path, content)
}

export function readFile(arg1, arg2 = undefined) {
let baseUri, path
if (arg2 === undefined) {
baseUri = arg1
path = ''
} else {
baseUri = arg1
path = arg2
}
return android.readFile(baseUri, path)
}
11 changes: 10 additions & 1 deletion src/renderer/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { IpcChannels } from '../../constants'
import FtToastEvents from '../components/ft-toast/ft-toast-events'
import i18n from '../i18n/index'
import router from '../router/index'
import { requestSaveDialog, writeFile } from './android'
import { readFile, requestOpenDialog, requestSaveDialog, writeFile } from './android'

// allowed characters in channel handle: A-Z, a-z, 0-9, -, _, .
// https://support.google.com/youtube/answer/11585688#change_handle
Expand Down Expand Up @@ -273,6 +273,8 @@ export async function showOpenDialog (options) {
if (process.env.IS_ELECTRON) {
const { ipcRenderer } = require('electron')
return await ipcRenderer.invoke(IpcChannels.SHOW_OPEN_DIALOG, options)
} else if (process.env.IS_ANDROID) {
return await requestOpenDialog(options.filters[0].extensions)
} else {
return await new Promise((resolve) => {
const fileInput = document.createElement('input')
Expand Down Expand Up @@ -317,6 +319,13 @@ export function readFileFromDialog(response, index = 0) {
resolve(new TextDecoder('utf-8').decode(data))
})
.catch(reject)
} else if (process.env.IS_ANDROID) {
const { uri } = response
try {
resolve(readFile(uri))
} catch (err) {
reject(err)
}
} else {
// if this is web, use FileReader
try {
Expand Down

0 comments on commit 7a91081

Please sign in to comment.