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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ firebase.json
**/ios/Flutter/flutter_assets/
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
**/ios/Flutter/ephemeral/

# Windows
**/windows/flutter/ephemeral/
Expand Down
4 changes: 4 additions & 0 deletions assets/userscripts/apis/GM.getValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// https://wiki.greasespot.net/GM.getValue
GM.getValue = function(key, defaultValue) {
return Promise.resolve(localStorage.getItem(key) ?? defaultValue);
}
5 changes: 5 additions & 0 deletions assets/userscripts/apis/GM.setValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// https://wiki.greasespot.net/GM.setValue
GM.setValue = function(key, value) {
localStorage.setItem(key, value)
return Promise.resolve();
}
3 changes: 3 additions & 0 deletions assets/userscripts/apis/GM_getValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
let GM_getValue = function(key, defaultValue) {
return localStorage.getItem(key) ?? defaultValue;
}
3 changes: 3 additions & 0 deletions assets/userscripts/apis/GM_setValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
let GM_setValue = function(key, value) {
localStorage.setItem(key, value);
}
22 changes: 22 additions & 0 deletions assets/userscripts/apis/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// > All scripts always get GM.info even without specifically requesting it.
// per https://wiki.greasespot.net/@grant
let GM = {
info: {
// https://wiki.greasespot.net/GM.info
scriptHandler: 'TornPDA',
scriptMetaStr: '',
scriptWillUpdate: false,
version: '0.0.0',
script: {
name: 'Default Script',
namespace: 'default',
description: 'A default userscript.',
excludes: [],
includes: [],
matches: [],
resources: {},
'run-at': "document-start",
version: '0.0.0'
}
}
};
2 changes: 2 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import 'package:torn_pda/providers/theme_provider.dart';
import 'package:torn_pda/providers/trades_provider.dart';
import 'package:torn_pda/providers/user_controller.dart';
import 'package:torn_pda/providers/user_details_provider.dart';
import 'package:torn_pda/providers/userscripts_apis_provider.dart';
import 'package:torn_pda/providers/userscripts_provider.dart';
import 'package:torn_pda/providers/war_controller.dart';
import 'package:torn_pda/providers/webview_provider.dart';
Expand Down Expand Up @@ -158,6 +159,7 @@ Future<void> main() async {
// START ## Force splash screen to stay on until we get essential start-up data
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await _shouldSyncDeviceTheme(widgetsBinding);
await UserscriptApisProvider.initialize();
FlutterNativeSplash.remove();
// END ## Release splash screen

Expand Down
58 changes: 48 additions & 10 deletions lib/models/userscript_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class UserScriptModel {
required this.isExample,
this.customApiKey = "",
this.customApiKeyCandidate = false,
this.grants = const [],
});

bool enabled;
Expand All @@ -49,6 +50,7 @@ class UserScriptModel {
bool isExample;
String customApiKey;
bool customApiKeyCandidate;
List<String> grants;

factory UserScriptModel.fromJson(Map<String, dynamic> json) {
// First check if is old model
Expand Down Expand Up @@ -77,6 +79,7 @@ class UserScriptModel {
url: url,
updateStatus: updateStatus,
isExample: isExample,
grants: <String>[], // Old model does not have grants
);
} else {
return UserScriptModel(
Expand All @@ -92,19 +95,22 @@ class UserScriptModel {
isExample: json["isExample"] ?? (json["exampleCode"] ?? 0) > 0,
customApiKey: json["customApiKey"] ?? "",
customApiKeyCandidate: json["customApiKeyCandidate"] ?? false,
grants: json["grants"] is List<dynamic> ? json["grants"].cast<String>() : const [],
);
}
}

factory UserScriptModel.fromMetaMap(Map<String, dynamic> metaMap,
{String? url,
UserScriptUpdateStatus updateStatus = UserScriptUpdateStatus.noRemote,
bool? isExample,
String? name,
String? source,
UserScriptTime? time,
String? customApiKey,
bool? customApiKeyCandidate}) {
factory UserScriptModel.fromMetaMap(
Map<String, dynamic> metaMap, {
String? url,
UserScriptUpdateStatus updateStatus = UserScriptUpdateStatus.noRemote,
bool? isExample,
String? name,
String? source,
UserScriptTime? time,
String? customApiKey,
bool? customApiKeyCandidate,
}) {
if (metaMap["name"] == null) {
throw Exception("No script name found in userscript");
}
Expand All @@ -123,6 +129,7 @@ class UserScriptModel {
isExample: isExample ?? false,
customApiKey: customApiKey ?? "",
customApiKeyCandidate: customApiKeyCandidate ?? false,
grants: metaMap["grant"] ?? <String>[],
);
}

Expand Down Expand Up @@ -179,6 +186,7 @@ class UserScriptModel {
"url": url,
"updateStatus": updateStatus.name,
"isExample": isExample,
"grants": grants,
"time": time == UserScriptTime.start ? "start" : "end",
"customApiKey": customApiKey,
"customApiKeyCandidate": customApiKeyCandidate,
Expand All @@ -192,7 +200,8 @@ class UserScriptModel {
throw Exception("No header found in userscript.");
}
Iterable<RegExpMatch> metaMatches = RegExp(r"^(?:^|\n)\s*\/\/\x20(@\S+)(.*)$", multiLine: true).allMatches(meta);
Map<String, dynamic> metaMap = {"@match": <String>[]};
Map<String, dynamic> metaMap = {"@match": <String>[], "@grant": <String>[]};
bool foundNoneGrant = false;
for (final match in metaMatches) {
if (match.groupCount < 2) {
continue;
Expand All @@ -202,10 +211,25 @@ class UserScriptModel {
}
if (match.group(1)?.toLowerCase() == "@match") {
metaMap["@match"].add(match.group(2)!.trim());
} else if (match.group(1)?.toLowerCase() == "@grant") {
if (match.group(2)?.trim() == "none") {
// see note below (after the loop) on this behavior
foundNoneGrant = true;
continue;
}
metaMap["@grant"].add(match.group(2)!.trim());
} else {
metaMap[match.group(1)!.trim().toLowerCase()] = match.group(2)!.trim();
}
}

if (foundNoneGrant) {
// per https://wiki.greasespot.net/@grant
// > If you specify none and something else, none takes precedence. This can be confusing.
// > Check for a none to remove if you're adding APIs you intend to use.
metaMap["@grant"] = <String>[];
}

return {
"name": metaMap["@name"],
"version": metaMap["@version"],
Expand All @@ -214,6 +238,7 @@ class UserScriptModel {
"injectionTime": metaMap["@run-at"] ?? "document-end",
"downloadURL": metaMap["@downloadurl"],
"updateURL": metaMap["@updateurl"],
"grant": metaMap["@grant"],
"source": source,
};
}
Expand All @@ -234,6 +259,7 @@ class UserScriptModel {
String? url,
String? customApiKey,
bool? customApiKeyCandidate,
List<String>? grants,
required UserScriptUpdateStatus updateStatus,
}) {
if (source != null) {
Expand All @@ -246,6 +272,9 @@ class UserScriptModel {
if (metaMap["matches"] != null) {
this.matches = metaMap["matches"];
}
if (metaMap["grant"] != null) {
this.grants = metaMap["grant"];
}
if (metaMap["name"] != null) {
this.name = metaMap["name"];
}
Expand Down Expand Up @@ -319,6 +348,15 @@ class UserScriptModel {
}
}

static List<String> tryGetGrants(String source) {
try {
final metaMap = UserScriptModel.parseHeader(source);
return metaMap["grant"] ?? <String>[];
} catch (e) {
return const [];
}
}

static String? tryGetUrl(String source) {
try {
final metaMap = UserScriptModel.parseHeader(source);
Expand Down
45 changes: 45 additions & 0 deletions lib/providers/userscripts_apis_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'dart:async' show Future;
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;

class UserscriptApisProvider {
static Map<String, String> _apis = {};

/// Initializes the provider by loading the API scripts from the assets.
/// Returns a [Future] if someone wants to check for errors.
static Future<Map<String, String>> initialize() async {
_apis = await _getApisMap(_getApis());
return _apis;
}

static Map<String, String> get apis {
return _apis;
}

static Future<Map<String, String>> _getApisMap(
Future<List<String>> apiEntries) async {
return apiEntries.then((fileList) async {
return {
for (var file in fileList)
_mapFileNameToApiName(file): await rootBundle.loadString(file)
};
});
}

static String _mapFileNameToApiName(String fileName) {
return fileName
.replaceFirst('assets/userscripts/apis/', '')
.replaceFirst('.js', '');
}

static Future<List<String>> _getApis() async {
final manifestContent = await rootBundle.loadString('AssetManifest.json');

final Map<String, dynamic> manifestMap = json.decode(manifestContent);

return manifestMap.keys
.where((String key) => key.startsWith('assets/userscripts/apis/'))
.where((String key) => key.endsWith('.js'))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly a stupid question - why not just .where((String key) => key.startsWith("...") && key.endsWith("..."))?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, just my habit of breaking things up like this. I like to think about it this way, though I guess technically it is less efficient (depending on the implementation) because you need to iterate twice. Happy to change it.

.toList();
}
}
Loading